From 31dfe054b52e1f01efe42fc3abb34b3d3cb0373a Mon Sep 17 00:00:00 2001 From: Ravi Prasad Date: Thu, 3 Jul 2025 20:19:59 -0500 Subject: [PATCH 01/10] postman docs --- .DS_Store | Bin 30724 -> 30724 bytes docs/.DS_Store | Bin 0 -> 10244 bytes docs/postman/.DS_Store | Bin 0 -> 6148 bytes .../NBA Statistics Go.postman_collection.json | 1596 +++++++++++++++++ 4 files changed, 1596 insertions(+) create mode 100644 docs/.DS_Store create mode 100644 docs/postman/.DS_Store create mode 100644 docs/postman/NBA Statistics Go.postman_collection.json diff --git a/.DS_Store b/.DS_Store index 7961524c1fdb797f07a48083e1810e4c2dd4abce..6fc23a9eb36db554e39108b6ae946cc38bdf55b1 100644 GIT binary patch delta 374 zcmZqqz}WJEae_ai#m0c$@{C-Yc@(1gCYxCaF>*{UjEa`#lHMg9+-^y5$g&~C@k)ebkF(=(HI5|JJfB^^?&Ho*kSa_b1e>0E4W2S)I zd>5Cboctu93^|G2Fhk`ho9yEZXjg$ delta 146 zcmZqqz}WJEae_ai!N!2y@{F9Dc@(1gChzns6EQc^Q7|z!t<_PeHZ(KXf&Gm;$M7IF(xD^F^WED#9$|&A(34Zz2?-KHc)XaTHCSDvuzq&UDV(Qaet`ajSo(N-G-akU zb8LNukARPWkARPWkARQBje!7uvsvM*UFp|80zLvh0#^}W>q8JPCc~bbcBQj)VC8H9 zP*$SaDR__j0LLfj$*?DuGu_<-c&^}WF~D`l{vb0)8TRC~D_wU2t~&uQGI%=_ zaH6AM&@m?vcBNnY2>1xhMS$(y7Z8J(#3pL_{(b4z(%!Tgi6nJ1iCX^5@3dug#V6h6 z7Rq%jG83~;2-?t{RFMqEw{Qe$A`>H7urgkh)5(mkS$137Mc^tb7c8vetN3dE-spr` zh}yE9vwGz0e%6Yark*U`%gJF)EhK0|uW8t_W~f=mK~q#TIWVFbidpC}bB1C$zEQV> z7kD8-8>gl^I@?==9b2{>Zw*d$cC@tyH*M}beq7*}gf?y)OdQeEhIxVw8y6XGF(RVd zzFI$ycV)9GIT?W;&R@gA%J;hSuDG)}?@Psb3yZ32#9O4{k(%MSI5s9v#?(nWsvT1k zwAMlZ`ctN{FRSKp;v*?j&!r73LF-bop33R6t#<1=Ge%vlmCBmBzR$EYTQiN(1g&RI zdo!$M4XarTW!#?i^b-5{m}O?|UME6=(wwCZsu)1as}T%i#WfrUixdjCxM&D()iwrJ zuU+4^X=_(x*PeYdw61=!xI|nwlGiLPsjEW;O|d6NWhOPj6Pm zTJap#HA`a5!ZA|WGi==;im@Y@7t|75*Y*xk8q(#Ys#8U9WMcyM@_TG$Y;HWz>~Yp_TX67Vd+2I2P!?yQY7)IPibs*w?izBN~=0UDkLjqgv`t2hU^Dzl>?mlKxFN32(zYa0cFo58*6)1n1y$I1gXImq>$4nEZdh zpJmB?Pg!zT%uVjupXZZ<4QmcAaz4*x6iA!@e4^*KD)~%v&X4M{=j5YVLpKdZ!Xg1n zhv>I_1bhU11bhU11bhT;Km^!N2rNN%{@*wM|NoAWU-=052;7_qKxHHv>A|_p_dA}Q zwFmJ!gcmE!ZrYWO3s%l?JWA&{9vkd9z8NoOTRdZ6_hC;r?Ml>O`Op6hNaBF~{lCBe Mzs|AO&Ak8r8`jqKN&o-= literal 0 HcmV?d00001 diff --git a/docs/postman/.DS_Store b/docs/postman/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0", + " .tftable {font-size:14px;color:#333333;width:100%;border-width: 1px;border-color: #87ceeb;border-collapse: collapse;}", + " .tftable th {font-size:18px;background-color:#87ceeb;border-width: 1px;padding: 8px;border-style: solid;border-color: #87ceeb;text-align:left;}", + " .tftable tr {background-color:#ffffff;}", + " .tftable td {font-size:14px;border-width: 1px;padding: 8px;border-style: solid;border-color: #87ceeb;}", + " .tftable tr:hover {background-color:#e0ffff;}", + "", + "", + "", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " {{#each response.data}}", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " {{/each}}", + "
IDCreatedAtUpdatedAtDeletedAtidplayerIdplayerNamepositionagegamesgamesStartedminutesPgfieldGoalsfieldAttemptsfieldPercentthreeFgthreeAttemptsthreePercenttwoFgtwoAttemptstwoPercenteffectFgPercentftftAttemptsftPercentoffensiveRbdefensiveRbtotalRbassistsstealsblocksturnoverspersonalFoulspointsteamseason
{{ID}}{{CreatedAt}}{{UpdatedAt}}{{DeletedAt}}{{id}}{{playerId}}{{playerName}}{{position}}{{age}}{{games}}{{gamesStarted}}{{minutesPg}}{{fieldGoals}}{{fieldAttempts}}{{fieldPercent}}{{threeFg}}{{threeAttempts}}{{threePercent}}{{twoFg}}{{twoAttempts}}{{twoPercent}}{{effectFgPercent}}{{ft}}{{ftAttempts}}{{ftPercent}}{{offensiveRb}}{{defensiveRb}}{{totalRb}}{{assists}}{{steals}}{{blocks}}{{turnovers}}{{personalFouls}}{{points}}{{team}}{{season}}
", + "`;", + "", + "function constructVisualizerPayload() {", + " return {response: pm.response.json()}", + "}", + "", + "pm.visualizer.set(template, constructVisualizerPayload());" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{api-key}}", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{base_url}}/api/playertotals?season=2025&team=HOU&page=1&pageSize=35&isPlayoff=False", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "playertotals" + ], + "query": [ + { + "key": "season", + "value": "2025", + "description": "(Optional) The season year (e.g., 2025 for the 2024-25 season)." + }, + { + "key": "team", + "value": "HOU", + "description": "(Optional) The three-letter team abbreviation (e.g., LAL, HOU, MIL)." + }, + { + "key": "page", + "value": "1", + "description": "(Optional) The page number for pagination. Defaults to 1." + }, + { + "key": "pageSize", + "value": "35", + "description": "(Optional) The number of results to return per page. Defaults to 20." + }, + { + "key": "isPlayoff", + "value": "False", + "description": "(Optional) Set to 'true' to retrieve playoff stats, or 'false' for regular season stats." + } + ] + }, + "description": "### Get Player Total Stats\n\nReturns a paginated and filterable list of traditional player statistics (totals) for a given season. You can retrieve either regular season or playoff data.\n\n### Usage\n\nThis endpoint supports filtering by `season`, `team`, and `playerId`. Results can be paginated using `page` and `pageSize`, and sorted using `sortBy` and `ascending`.\n\n#### **Query Parameters**\n\n- **`season`** (integer, optional): The season year (e.g., 2000).\n \n- **`team`** (string, optional): Team abbreviation (e.g., LAL).\n \n- **`playerId`** (string, optional): The player's unique ID (e.g., `greenac01`).\n \n- **`page`** (integer, optional): Page number. Defaults to `1`.\n \n- **`pageSize`** (integer, optional): Number of results per page. Defaults to `20`.\n \n- **`isPlayoff`** (boolean, optional): Set to `true` for playoff stats. The filter is only applied if the parameter is provided.\n \n- **`sortBy`** (string, optional): Field to sort by. Defaults to `points`.\n \n- **`ascending`** (boolean, optional): Use `true` for ascending order. Defaults to `false`.\n \n\n#### **Available** **`sortBy`** **Fields:**\n\n`playerId`, `playerName`, `position`, `age`, `games`, `gamesStarted`, `minutesPg`, `fieldGoals`, `fieldAttempts`, `fieldPercent`, `threeFg`, `threeAttempts`, `threePercent`, `twoFg`, `twoAttempts`, `twoPercent`, `effectFgPercent`, `ft`, `ftAttempts`, `ftPercent`, `offensiveRb`, `defensiveRb`, `totalRb`, `assists`, `steals`, `blocks`, `turnovers`, `personalFouls`, `points`, `team`, `season`" + }, + "response": [ + { + "name": "Player Totals Stats", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{api-key}}", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{base_url}}/api/playertotals?season=2025&team=HOU&page=1&pageSize=35&isPlayoff=False", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "playertotals" + ], + "query": [ + { + "key": "season", + "value": "2025", + "description": "(Optional) The season year (e.g., 2025 for the 2024-25 season)." + }, + { + "key": "team", + "value": "HOU", + "description": "(Optional) The three-letter team abbreviation (e.g., LAL, HOU, MIL)." + }, + { + "key": "page", + "value": "1", + "description": "(Optional) The page number for pagination. Defaults to 1." + }, + { + "key": "pageSize", + "value": "35", + "description": "(Optional) The number of results to return per page. Defaults to 20." + }, + { + "key": "isPlayoff", + "value": "False", + "description": "(Optional) Set to 'true' to retrieve playoff stats, or 'false' for regular season stats." + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Alt-Svc", + "value": "h3=\":443\"; ma=2592000" + }, + { + "key": "Content-Encoding", + "value": "br" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Date", + "value": "Wed, 02 Jul 2025 01:16:40 GMT" + }, + { + "key": "Server", + "value": "nginx/1.28.0" + }, + { + "key": "Vary", + "value": "Accept-Encoding" + }, + { + "key": "Transfer-Encoding", + "value": "chunked" + } + ], + "cookie": [], + "body": "{\n \"data\": [\n {\n \"ID\": 1648,\n \"id\": 13,\n \"playerId\": \"greenja05\",\n \"playerName\": \"Jalen Green\",\n \"position\": \"SG\",\n \"age\": 22,\n \"games\": 82,\n \"gamesStarted\": 82,\n \"minutesPg\": 2697,\n \"fieldGoals\": 608,\n \"fieldAttempts\": 1437,\n \"fieldPercent\": 0.423,\n \"threeFg\": 234,\n \"threeAttempts\": 661,\n \"threePercent\": 0.354,\n \"twoFg\": 374,\n \"twoAttempts\": 776,\n \"twoPercent\": 0.482,\n \"effectFgPercent\": 0.505,\n \"ft\": 273,\n \"ftAttempts\": 336,\n \"ftPercent\": 0.813,\n \"offensiveRb\": 45,\n \"defensiveRb\": 332,\n \"totalRb\": 377,\n \"assists\": 282,\n \"steals\": 71,\n \"blocks\": 27,\n \"turnovers\": 203,\n \"personalFouls\": 127,\n \"points\": 1723,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:06:56.81977Z\",\n \"UpdatedAt\": \"2025-06-20T01:06:56.81977Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1664,\n \"id\": 27,\n \"playerId\": \"sengual01\",\n \"playerName\": \"Alperen Şengün\",\n \"position\": \"C\",\n \"age\": 22,\n \"games\": 76,\n \"gamesStarted\": 76,\n \"minutesPg\": 2395,\n \"fieldGoals\": 567,\n \"fieldAttempts\": 1143,\n \"fieldPercent\": 0.496,\n \"threeFg\": 21,\n \"threeAttempts\": 90,\n \"threePercent\": 0.233,\n \"twoFg\": 546,\n \"twoAttempts\": 1053,\n \"twoPercent\": 0.519,\n \"effectFgPercent\": 0.505,\n \"ft\": 296,\n \"ftAttempts\": 428,\n \"ftPercent\": 0.692,\n \"offensiveRb\": 262,\n \"defensiveRb\": 524,\n \"totalRb\": 786,\n \"assists\": 372,\n \"steals\": 84,\n \"blocks\": 61,\n \"turnovers\": 194,\n \"personalFouls\": 209,\n \"points\": 1451,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:06:59.072751Z\",\n \"UpdatedAt\": \"2025-06-20T01:06:59.072751Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1725,\n \"id\": 78,\n \"playerId\": \"brookdi01\",\n \"playerName\": \"Dillon Brooks\",\n \"position\": \"SF\",\n \"age\": 29,\n \"games\": 75,\n \"gamesStarted\": 75,\n \"minutesPg\": 2388,\n \"fieldGoals\": 384,\n \"fieldAttempts\": 895,\n \"fieldPercent\": 0.429,\n \"threeFg\": 186,\n \"threeAttempts\": 469,\n \"threePercent\": 0.397,\n \"twoFg\": 198,\n \"twoAttempts\": 426,\n \"twoPercent\": 0.465,\n \"effectFgPercent\": 0.533,\n \"ft\": 99,\n \"ftAttempts\": 121,\n \"ftPercent\": 0.818,\n \"offensiveRb\": 75,\n \"defensiveRb\": 200,\n \"totalRb\": 275,\n \"assists\": 128,\n \"steals\": 60,\n \"blocks\": 16,\n \"turnovers\": 76,\n \"personalFouls\": 242,\n \"points\": 1053,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:07:08.04821Z\",\n \"UpdatedAt\": \"2025-06-20T01:07:08.04821Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1741,\n \"id\": 91,\n \"playerId\": \"thompam01\",\n \"playerName\": \"Amen Thompson\",\n \"position\": \"SF\",\n \"age\": 22,\n \"games\": 69,\n \"gamesStarted\": 42,\n \"minutesPg\": 2225,\n \"fieldGoals\": 388,\n \"fieldAttempts\": 697,\n \"fieldPercent\": 0.557,\n \"threeFg\": 25,\n \"threeAttempts\": 91,\n \"threePercent\": 0.275,\n \"twoFg\": 363,\n \"twoAttempts\": 606,\n \"twoPercent\": 0.599,\n \"effectFgPercent\": 0.575,\n \"ft\": 169,\n \"ftAttempts\": 247,\n \"ftPercent\": 0.684,\n \"offensiveRb\": 192,\n \"defensiveRb\": 372,\n \"totalRb\": 564,\n \"assists\": 265,\n \"steals\": 97,\n \"blocks\": 89,\n \"turnovers\": 138,\n \"personalFouls\": 167,\n \"points\": 970,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:07:10.362164Z\",\n \"UpdatedAt\": \"2025-06-20T01:07:10.362164Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1763,\n \"id\": 109,\n \"playerId\": \"vanvlfr01\",\n \"playerName\": \"Fred VanVleet\",\n \"position\": \"PG\",\n \"age\": 30,\n \"games\": 60,\n \"gamesStarted\": 60,\n \"minutesPg\": 2111,\n \"fieldGoals\": 287,\n \"fieldAttempts\": 759,\n \"fieldPercent\": 0.378,\n \"threeFg\": 159,\n \"threeAttempts\": 461,\n \"threePercent\": 0.345,\n \"twoFg\": 128,\n \"twoAttempts\": 298,\n \"twoPercent\": 0.43,\n \"effectFgPercent\": 0.483,\n \"ft\": 111,\n \"ftAttempts\": 137,\n \"ftPercent\": 0.81,\n \"offensiveRb\": 32,\n \"defensiveRb\": 189,\n \"totalRb\": 221,\n \"assists\": 333,\n \"steals\": 94,\n \"blocks\": 25,\n \"turnovers\": 87,\n \"personalFouls\": 140,\n \"points\": 844,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:07:13.597703Z\",\n \"UpdatedAt\": \"2025-06-20T01:07:13.597703Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1821,\n \"id\": 155,\n \"playerId\": \"smithja05\",\n \"playerName\": \"Jabari Smith Jr.\",\n \"position\": \"PF\",\n \"age\": 21,\n \"games\": 57,\n \"gamesStarted\": 39,\n \"minutesPg\": 1716,\n \"fieldGoals\": 248,\n \"fieldAttempts\": 566,\n \"fieldPercent\": 0.438,\n \"threeFg\": 98,\n \"threeAttempts\": 277,\n \"threePercent\": 0.354,\n \"twoFg\": 150,\n \"twoAttempts\": 289,\n \"twoPercent\": 0.519,\n \"effectFgPercent\": 0.525,\n \"ft\": 104,\n \"ftAttempts\": 126,\n \"ftPercent\": 0.825,\n \"offensiveRb\": 105,\n \"defensiveRb\": 294,\n \"totalRb\": 399,\n \"assists\": 60,\n \"steals\": 25,\n \"blocks\": 41,\n \"turnovers\": 61,\n \"personalFouls\": 123,\n \"points\": 698,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:07:21.954232Z\",\n \"UpdatedAt\": \"2025-06-20T01:07:21.954232Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1825,\n \"id\": 159,\n \"playerId\": \"easonta01\",\n \"playerName\": \"Tari Eason\",\n \"position\": \"PF\",\n \"age\": 23,\n \"games\": 57,\n \"gamesStarted\": 16,\n \"minutesPg\": 1420,\n \"fieldGoals\": 272,\n \"fieldAttempts\": 559,\n \"fieldPercent\": 0.487,\n \"threeFg\": 63,\n \"threeAttempts\": 184,\n \"threePercent\": 0.342,\n \"twoFg\": 209,\n \"twoAttempts\": 375,\n \"twoPercent\": 0.557,\n \"effectFgPercent\": 0.543,\n \"ft\": 79,\n \"ftAttempts\": 104,\n \"ftPercent\": 0.76,\n \"offensiveRb\": 128,\n \"defensiveRb\": 234,\n \"totalRb\": 362,\n \"assists\": 83,\n \"steals\": 97,\n \"blocks\": 50,\n \"turnovers\": 65,\n \"personalFouls\": 136,\n \"points\": 686,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:07:22.520553Z\",\n \"UpdatedAt\": \"2025-06-20T01:07:22.520553Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1919,\n \"id\": 239,\n \"playerId\": \"whitmca01\",\n \"playerName\": \"Cam Whitmore\",\n \"position\": \"SF\",\n \"age\": 20,\n \"games\": 51,\n \"gamesStarted\": 3,\n \"minutesPg\": 827,\n \"fieldGoals\": 179,\n \"fieldAttempts\": 403,\n \"fieldPercent\": 0.444,\n \"threeFg\": 65,\n \"threeAttempts\": 183,\n \"threePercent\": 0.355,\n \"twoFg\": 114,\n \"twoAttempts\": 220,\n \"twoPercent\": 0.518,\n \"effectFgPercent\": 0.525,\n \"ft\": 54,\n \"ftAttempts\": 72,\n \"ftPercent\": 0.75,\n \"offensiveRb\": 36,\n \"defensiveRb\": 115,\n \"totalRb\": 151,\n \"assists\": 49,\n \"steals\": 31,\n \"blocks\": 13,\n \"turnovers\": 46,\n \"personalFouls\": 44,\n \"points\": 477,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:07:36.322627Z\",\n \"UpdatedAt\": \"2025-06-20T01:07:36.322627Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1992,\n \"id\": 292,\n \"playerId\": \"holidaa01\",\n \"playerName\": \"Aaron Holiday\",\n \"position\": \"PG\",\n \"age\": 28,\n \"games\": 62,\n \"gamesStarted\": 3,\n \"minutesPg\": 792,\n \"fieldGoals\": 117,\n \"fieldAttempts\": 268,\n \"fieldPercent\": 0.437,\n \"threeFg\": 72,\n \"threeAttempts\": 181,\n \"threePercent\": 0.398,\n \"twoFg\": 45,\n \"twoAttempts\": 87,\n \"twoPercent\": 0.517,\n \"effectFgPercent\": 0.571,\n \"ft\": 34,\n \"ftAttempts\": 41,\n \"ftPercent\": 0.829,\n \"offensiveRb\": 13,\n \"defensiveRb\": 65,\n \"totalRb\": 78,\n \"assists\": 83,\n \"steals\": 19,\n \"blocks\": 11,\n \"turnovers\": 37,\n \"personalFouls\": 64,\n \"points\": 340,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:07:46.800477Z\",\n \"UpdatedAt\": \"2025-06-20T01:07:46.800477Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2066,\n \"id\": 344,\n \"playerId\": \"sheppre01\",\n \"playerName\": \"Reed Sheppard\",\n \"position\": \"PG\",\n \"age\": 20,\n \"games\": 52,\n \"gamesStarted\": 3,\n \"minutesPg\": 654,\n \"fieldGoals\": 84,\n \"fieldAttempts\": 239,\n \"fieldPercent\": 0.351,\n \"threeFg\": 48,\n \"threeAttempts\": 142,\n \"threePercent\": 0.338,\n \"twoFg\": 36,\n \"twoAttempts\": 97,\n \"twoPercent\": 0.371,\n \"effectFgPercent\": 0.452,\n \"ft\": 13,\n \"ftAttempts\": 16,\n \"ftPercent\": 0.813,\n \"offensiveRb\": 16,\n \"defensiveRb\": 62,\n \"totalRb\": 78,\n \"assists\": 75,\n \"steals\": 35,\n \"blocks\": 17,\n \"turnovers\": 37,\n \"personalFouls\": 53,\n \"points\": 229,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:07:57.495833Z\",\n \"UpdatedAt\": \"2025-06-20T01:07:57.495833Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2068,\n \"id\": 346,\n \"playerId\": \"adamsst01\",\n \"playerName\": \"Steven Adams\",\n \"position\": \"C\",\n \"age\": 31,\n \"games\": 58,\n \"gamesStarted\": 3,\n \"minutesPg\": 794,\n \"fieldGoals\": 91,\n \"fieldAttempts\": 167,\n \"fieldPercent\": 0.545,\n \"threeFg\": 0,\n \"threeAttempts\": 2,\n \"threePercent\": 0,\n \"twoFg\": 91,\n \"twoAttempts\": 165,\n \"twoPercent\": 0.552,\n \"effectFgPercent\": 0.545,\n \"ft\": 43,\n \"ftAttempts\": 93,\n \"ftPercent\": 0.462,\n \"offensiveRb\": 166,\n \"defensiveRb\": 161,\n \"totalRb\": 327,\n \"assists\": 66,\n \"steals\": 22,\n \"blocks\": 28,\n \"turnovers\": 54,\n \"personalFouls\": 60,\n \"points\": 225,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:07:57.793469Z\",\n \"UpdatedAt\": \"2025-06-20T01:07:57.793469Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2080,\n \"id\": 356,\n \"playerId\": \"landajo01\",\n \"playerName\": \"Jock Landale\",\n \"position\": \"C\",\n \"age\": 29,\n \"games\": 42,\n \"gamesStarted\": 3,\n \"minutesPg\": 500,\n \"fieldGoals\": 81,\n \"fieldAttempts\": 152,\n \"fieldPercent\": 0.533,\n \"threeFg\": 11,\n \"threeAttempts\": 26,\n \"threePercent\": 0.423,\n \"twoFg\": 70,\n \"twoAttempts\": 126,\n \"twoPercent\": 0.556,\n \"effectFgPercent\": 0.569,\n \"ft\": 27,\n \"ftAttempts\": 40,\n \"ftPercent\": 0.675,\n \"offensiveRb\": 56,\n \"defensiveRb\": 81,\n \"totalRb\": 137,\n \"assists\": 37,\n \"steals\": 13,\n \"blocks\": 10,\n \"turnovers\": 21,\n \"personalFouls\": 49,\n \"points\": 200,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:07:59.521078Z\",\n \"UpdatedAt\": \"2025-06-20T01:07:59.521078Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2090,\n \"id\": 364,\n \"playerId\": \"tateja01\",\n \"playerName\": \"Jae'Sean Tate\",\n \"position\": \"SF\",\n \"age\": 29,\n \"games\": 52,\n \"gamesStarted\": 2,\n \"minutesPg\": 588,\n \"fieldGoals\": 70,\n \"fieldAttempts\": 148,\n \"fieldPercent\": 0.473,\n \"threeFg\": 16,\n \"threeAttempts\": 46,\n \"threePercent\": 0.348,\n \"twoFg\": 54,\n \"twoAttempts\": 102,\n \"twoPercent\": 0.529,\n \"effectFgPercent\": 0.527,\n \"ft\": 32,\n \"ftAttempts\": 47,\n \"ftPercent\": 0.681,\n \"offensiveRb\": 50,\n \"defensiveRb\": 67,\n \"totalRb\": 117,\n \"assists\": 47,\n \"steals\": 26,\n \"blocks\": 7,\n \"turnovers\": 22,\n \"personalFouls\": 82,\n \"points\": 188,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:08:00.954562Z\",\n \"UpdatedAt\": \"2025-06-20T01:08:00.954562Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2097,\n \"id\": 371,\n \"playerId\": \"greenje02\",\n \"playerName\": \"Jeff Green\",\n \"position\": \"PF\",\n \"age\": 38,\n \"games\": 32,\n \"gamesStarted\": 3,\n \"minutesPg\": 396,\n \"fieldGoals\": 61,\n \"fieldAttempts\": 121,\n \"fieldPercent\": 0.504,\n \"threeFg\": 29,\n \"threeAttempts\": 79,\n \"threePercent\": 0.367,\n \"twoFg\": 32,\n \"twoAttempts\": 42,\n \"twoPercent\": 0.762,\n \"effectFgPercent\": 0.624,\n \"ft\": 21,\n \"ftAttempts\": 26,\n \"ftPercent\": 0.808,\n \"offensiveRb\": 11,\n \"defensiveRb\": 47,\n \"totalRb\": 58,\n \"assists\": 20,\n \"steals\": 6,\n \"blocks\": 4,\n \"turnovers\": 9,\n \"personalFouls\": 32,\n \"points\": 172,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:08:02.008397Z\",\n \"UpdatedAt\": \"2025-06-20T01:08:02.008397Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2242,\n \"id\": 461,\n \"playerId\": \"willije02\",\n \"playerName\": \"Jeenathan Williams\",\n \"position\": \"SG\",\n \"age\": 25,\n \"games\": 20,\n \"gamesStarted\": 0,\n \"minutesPg\": 147,\n \"fieldGoals\": 27,\n \"fieldAttempts\": 62,\n \"fieldPercent\": 0.435,\n \"threeFg\": 6,\n \"threeAttempts\": 26,\n \"threePercent\": 0.231,\n \"twoFg\": 21,\n \"twoAttempts\": 36,\n \"twoPercent\": 0.583,\n \"effectFgPercent\": 0.484,\n \"ft\": 5,\n \"ftAttempts\": 8,\n \"ftPercent\": 0.625,\n \"offensiveRb\": 5,\n \"defensiveRb\": 8,\n \"totalRb\": 13,\n \"assists\": 9,\n \"steals\": 8,\n \"blocks\": 4,\n \"turnovers\": 14,\n \"personalFouls\": 16,\n \"points\": 65,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:08:23.073002Z\",\n \"UpdatedAt\": \"2025-06-20T01:08:23.073002Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2303,\n \"id\": 508,\n \"playerId\": \"nfalyda01\",\n \"playerName\": \"N'Faly Dante\",\n \"position\": \"C\",\n \"age\": 23,\n \"games\": 4,\n \"gamesStarted\": 0,\n \"minutesPg\": 51,\n \"fieldGoals\": 10,\n \"fieldAttempts\": 13,\n \"fieldPercent\": 0.769,\n \"threeFg\": 0,\n \"threeAttempts\": 0,\n \"threePercent\": 0,\n \"twoFg\": 10,\n \"twoAttempts\": 13,\n \"twoPercent\": 0.769,\n \"effectFgPercent\": 0.769,\n \"ft\": 4,\n \"ftAttempts\": 5,\n \"ftPercent\": 0.8,\n \"offensiveRb\": 6,\n \"defensiveRb\": 15,\n \"totalRb\": 21,\n \"assists\": 2,\n \"steals\": 1,\n \"blocks\": 5,\n \"turnovers\": 2,\n \"personalFouls\": 9,\n \"points\": 24,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:08:31.603297Z\",\n \"UpdatedAt\": \"2025-06-20T01:08:31.603297Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2322,\n \"id\": 525,\n \"playerId\": \"mcveija01\",\n \"playerName\": \"Jack McVeigh\",\n \"position\": \"PF\",\n \"age\": 28,\n \"games\": 9,\n \"gamesStarted\": 0,\n \"minutesPg\": 43,\n \"fieldGoals\": 5,\n \"fieldAttempts\": 17,\n \"fieldPercent\": 0.294,\n \"threeFg\": 4,\n \"threeAttempts\": 13,\n \"threePercent\": 0.308,\n \"twoFg\": 1,\n \"twoAttempts\": 4,\n \"twoPercent\": 0.25,\n \"effectFgPercent\": 0.412,\n \"ft\": 0,\n \"ftAttempts\": 0,\n \"ftPercent\": 0,\n \"offensiveRb\": 2,\n \"defensiveRb\": 3,\n \"totalRb\": 5,\n \"assists\": 1,\n \"steals\": 0,\n \"blocks\": 2,\n \"turnovers\": 2,\n \"personalFouls\": 3,\n \"points\": 14,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:08:34.253789Z\",\n \"UpdatedAt\": \"2025-06-20T01:08:34.253789Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2116,\n \"id\": 385,\n \"playerId\": \"roddyda01\",\n \"playerName\": \"David Roddy\",\n \"position\": \"PF\",\n \"age\": 23,\n \"games\": 3,\n \"gamesStarted\": 0,\n \"minutesPg\": 35,\n \"fieldGoals\": 5,\n \"fieldAttempts\": 13,\n \"fieldPercent\": 0.385,\n \"threeFg\": 1,\n \"threeAttempts\": 7,\n \"threePercent\": 0.143,\n \"twoFg\": 4,\n \"twoAttempts\": 6,\n \"twoPercent\": 0.667,\n \"effectFgPercent\": 0.423,\n \"ft\": 2,\n \"ftAttempts\": 4,\n \"ftPercent\": 0.5,\n \"offensiveRb\": 0,\n \"defensiveRb\": 5,\n \"totalRb\": 5,\n \"assists\": 2,\n \"steals\": 0,\n \"blocks\": 1,\n \"turnovers\": 1,\n \"personalFouls\": 1,\n \"points\": 13,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:08:04.73934Z\",\n \"UpdatedAt\": \"2025-06-20T01:08:04.73934Z\",\n \"DeletedAt\": null\n }\n ],\n \"pagination\": {\n \"page\": 1,\n \"pageSize\": 35,\n \"pages\": 1,\n \"total\": 18\n }\n}" + } + ] + }, + { + "name": "Player Totals Playoffs Stats", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "", + "pm.test(\"Response time is less than 200ms\", function () {", + " pm.expect(pm.response.responseTime).to.be.below(200);", + "});", + "", + "", + "pm.test(\"Response has the required fields\", function () {", + " const responseData = pm.response.json();", + " ", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.data).to.be.an('array');", + " ", + " responseData.data.forEach(function(player) {", + " pm.expect(player).to.have.property('ID');", + " pm.expect(player).to.have.property('CreatedAt');", + " pm.expect(player).to.have.property('UpdatedAt');", + " pm.expect(player).to.have.property('DeletedAt');", + " pm.expect(player).to.have.property('playerId');", + " pm.expect(player).to.have.property('playerName');", + " pm.expect(player).to.have.property('position');", + " pm.expect(player).to.have.property('age');", + " pm.expect(player).to.have.property('games');", + " });", + "});", + "", + "", + "", + "pm.test(\"Pagination object is present and contains expected fields\", function () {", + " const responseData = pm.response.json();", + " ", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.pagination).to.exist;", + " pm.expect(responseData.pagination).to.have.property('page');", + " pm.expect(responseData.pagination).to.have.property('pageSize');", + " pm.expect(responseData.pagination).to.have.property('pages');", + " pm.expect(responseData.pagination).to.have.property('total');", + "});", + "var template = `", + "", + "", + "", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " {{#each response.data}}", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " {{/each}}", + "
IDCreatedAtUpdatedAtDeletedAtidplayerIdplayerNamepositionagegamesgamesStartedminutesPgfieldGoalsfieldAttemptsfieldPercentthreeFgthreeAttemptsthreePercenttwoFgtwoAttemptstwoPercenteffectFgPercentftftAttemptsftPercentoffensiveRbdefensiveRbtotalRbassistsstealsblocksturnoverspersonalFoulspointsteamseason
{{ID}}{{CreatedAt}}{{UpdatedAt}}{{DeletedAt}}{{id}}{{playerId}}{{playerName}}{{position}}{{age}}{{games}}{{gamesStarted}}{{minutesPg}}{{fieldGoals}}{{fieldAttempts}}{{fieldPercent}}{{threeFg}}{{threeAttempts}}{{threePercent}}{{twoFg}}{{twoAttempts}}{{twoPercent}}{{effectFgPercent}}{{ft}}{{ftAttempts}}{{ftPercent}}{{offensiveRb}}{{defensiveRb}}{{totalRb}}{{assists}}{{steals}}{{blocks}}{{turnovers}}{{personalFouls}}{{points}}{{team}}{{season}}
", + "`;", + "", + "function constructVisualizerPayload() {", + " return {response: pm.response.json()}", + "}", + "", + "pm.visualizer.set(template, constructVisualizerPayload());" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{api-key}}", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{base_url}}/api/playertotals?season=2019&page=1&pageSize=20&sortBy=assists&isPlayoff=true", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "playertotals" + ], + "query": [ + { + "key": "season", + "value": "2019", + "description": "(Optional) The season year (e.g., 2025 for the 2024-25 season)." + }, + { + "key": "team", + "value": "HOU", + "description": "(Optional) The three-letter team abbreviation (e.g., LAL, HOU, MIL).", + "disabled": true + }, + { + "key": "page", + "value": "1", + "description": "(Optional) The page number for pagination. Defaults to 1." + }, + { + "key": "pageSize", + "value": "20", + "description": "(Optional) The number of results to return per page. Defaults to 20." + }, + { + "key": "sortBy", + "value": "assists", + "description": "(Optional) The field to sort the results by. Defaults to 'points'." + }, + { + "key": "isPlayoff", + "value": "true", + "description": "(Optional) Set to 'true' to retrieve playoff stats, or 'false' for regular season stats." + } + ] + }, + "description": "### Get Player Total Stats (Playoffs)\n\nReturns a paginated and filterable list of traditional player statistics (totals) for the playoffs. This is an example request that sets `isPlayoff=true`.\n\n### Usage\n\nThis endpoint supports filtering by `season`, `team`, and `playerId`. Results can be paginated using `page` and `pageSize`, and sorted using `sortBy` and `ascending`.\n\n#### **Query Parameters**\n\n* **`season`** (integer, optional): The season year (e.g., 2000).\n* **`team`** (string, optional): Team abbreviation (e.g., LAL).\n* **`playerId`** (string, optional): The player's unique ID (e.g., `greenac01`).\n* **`page`** (integer, optional): Page number. Defaults to `1`.\n* **`pageSize`** (integer, optional): Number of results per page. Defaults to `20`.\n* **`isPlayoff`** (boolean, optional): Set to `true` for playoff stats. The filter is only applied if the parameter is provided.\n* **`sortBy`** (string, optional): Field to sort by. Defaults to `points`.\n* **`ascending`** (boolean, optional): Use `true` for ascending order. Defaults to `false`.\n\n#### **Available `sortBy` Fields:**\n`playerId`, `playerName`, `position`, `age`, `games`, `gamesStarted`, `minutesPg`, `fieldGoals`, `fieldAttempts`, `fieldPercent`, `threeFg`, `threeAttempts`, `threePercent`, `twoFg`, `twoAttempts`, `twoPercent`, `effectFgPercent`, `ft`, `ftAttempts`, `ftPercent`, `offensiveRb`, `defensiveRb`, `totalRb`, `assists`, `steals`, `blocks`, `turnovers`, `personalFouls`, `points`, `team`, `season`" + }, + "response": [ + { + "name": "Player Totals Playoffs Stats", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{api-key}}", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{base_url}}/api/playertotals?season=2019&page=1&pageSize=20&sortBy=assists&isPlayoff=true", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "playertotals" + ], + "query": [ + { + "key": "season", + "value": "2019", + "description": "(Optional) The season year (e.g., 2025 for the 2024-25 season)." + }, + { + "key": "team", + "value": "HOU", + "description": "(Optional) The three-letter team abbreviation (e.g., LAL, HOU, MIL).", + "disabled": true + }, + { + "key": "page", + "value": "1", + "description": "(Optional) The page number for pagination. Defaults to 1." + }, + { + "key": "pageSize", + "value": "20", + "description": "(Optional) The number of results to return per page. Defaults to 20." + }, + { + "key": "sortBy", + "value": "assists", + "description": "(Optional) The field to sort the results by. Defaults to 'points'." + }, + { + "key": "isPlayoff", + "value": "true", + "description": "(Optional) Set to 'true' to retrieve playoff stats, or 'false' for regular season stats." + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Alt-Svc", + "value": "h3=\":443\"; ma=2592000" + }, + { + "key": "Content-Encoding", + "value": "br" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Date", + "value": "Wed, 02 Jul 2025 01:17:12 GMT" + }, + { + "key": "Server", + "value": "nginx/1.28.0" + }, + { + "key": "Vary", + "value": "Accept-Encoding" + }, + { + "key": "Transfer-Encoding", + "value": "chunked" + } + ], + "cookie": [], + "body": "{\n \"data\": [\n {\n \"ID\": 7355,\n \"id\": 76,\n \"playerId\": \"greendr01\",\n \"playerName\": \"Draymond Green\",\n \"position\": \"PF\",\n \"age\": 28,\n \"games\": 22,\n \"gamesStarted\": 22,\n \"minutesPg\": 851,\n \"fieldGoals\": 114,\n \"fieldAttempts\": 229,\n \"fieldPercent\": 0.498,\n \"threeFg\": 13,\n \"threeAttempts\": 57,\n \"threePercent\": 0.228,\n \"twoFg\": 101,\n \"twoAttempts\": 172,\n \"twoPercent\": 0.587,\n \"effectFgPercent\": 0.526,\n \"ft\": 51,\n \"ftAttempts\": 71,\n \"ftPercent\": 0.718,\n \"offensiveRb\": 41,\n \"defensiveRb\": 182,\n \"totalRb\": 223,\n \"assists\": 187,\n \"steals\": 32,\n \"blocks\": 33,\n \"turnovers\": 83,\n \"personalFouls\": 85,\n \"points\": 292,\n \"team\": \"GSW\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:33.196486Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:33.196486Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7404,\n \"id\": 125,\n \"playerId\": \"lowryky01\",\n \"playerName\": \"Kyle Lowry\",\n \"position\": \"PG\",\n \"age\": 32,\n \"games\": 24,\n \"gamesStarted\": 24,\n \"minutesPg\": 901,\n \"fieldGoals\": 122,\n \"fieldAttempts\": 277,\n \"fieldPercent\": 0.44,\n \"threeFg\": 52,\n \"threeAttempts\": 145,\n \"threePercent\": 0.359,\n \"twoFg\": 70,\n \"twoAttempts\": 132,\n \"twoPercent\": 0.53,\n \"effectFgPercent\": 0.534,\n \"ft\": 65,\n \"ftAttempts\": 81,\n \"ftPercent\": 0.802,\n \"offensiveRb\": 21,\n \"defensiveRb\": 96,\n \"totalRb\": 117,\n \"assists\": 159,\n \"steals\": 31,\n \"blocks\": 7,\n \"turnovers\": 54,\n \"personalFouls\": 96,\n \"points\": 361,\n \"team\": \"TOR\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:42.589209Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:42.589209Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7321,\n \"id\": 42,\n \"playerId\": \"curryst01\",\n \"playerName\": \"Stephen Curry\",\n \"position\": \"PG\",\n \"age\": 30,\n \"games\": 22,\n \"gamesStarted\": 22,\n \"minutesPg\": 846,\n \"fieldGoals\": 190,\n \"fieldAttempts\": 431,\n \"fieldPercent\": 0.441,\n \"threeFg\": 92,\n \"threeAttempts\": 244,\n \"threePercent\": 0.377,\n \"twoFg\": 98,\n \"twoAttempts\": 187,\n \"twoPercent\": 0.524,\n \"effectFgPercent\": 0.548,\n \"ft\": 148,\n \"ftAttempts\": 157,\n \"ftPercent\": 0.943,\n \"offensiveRb\": 17,\n \"defensiveRb\": 115,\n \"totalRb\": 132,\n \"assists\": 126,\n \"steals\": 24,\n \"blocks\": 4,\n \"turnovers\": 66,\n \"personalFouls\": 68,\n \"points\": 620,\n \"team\": \"GSW\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:26.755237Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:26.755237Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7385,\n \"id\": 106,\n \"playerId\": \"jokicni01\",\n \"playerName\": \"Nikola Jokić\",\n \"position\": \"C\",\n \"age\": 23,\n \"games\": 14,\n \"gamesStarted\": 14,\n \"minutesPg\": 557,\n \"fieldGoals\": 132,\n \"fieldAttempts\": 261,\n \"fieldPercent\": 0.506,\n \"threeFg\": 22,\n \"threeAttempts\": 56,\n \"threePercent\": 0.393,\n \"twoFg\": 110,\n \"twoAttempts\": 205,\n \"twoPercent\": 0.537,\n \"effectFgPercent\": 0.548,\n \"ft\": 66,\n \"ftAttempts\": 78,\n \"ftPercent\": 0.846,\n \"offensiveRb\": 54,\n \"defensiveRb\": 128,\n \"totalRb\": 182,\n \"assists\": 118,\n \"steals\": 16,\n \"blocks\": 13,\n \"turnovers\": 36,\n \"personalFouls\": 54,\n \"points\": 352,\n \"team\": \"DEN\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:38.912453Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:38.912453Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7399,\n \"id\": 120,\n \"playerId\": \"lillada01\",\n \"playerName\": \"Damian Lillard\",\n \"position\": \"PG\",\n \"age\": 28,\n \"games\": 16,\n \"gamesStarted\": 16,\n \"minutesPg\": 650,\n \"fieldGoals\": 138,\n \"fieldAttempts\": 330,\n \"fieldPercent\": 0.418,\n \"threeFg\": 59,\n \"threeAttempts\": 158,\n \"threePercent\": 0.373,\n \"twoFg\": 79,\n \"twoAttempts\": 172,\n \"twoPercent\": 0.459,\n \"effectFgPercent\": 0.508,\n \"ft\": 95,\n \"ftAttempts\": 114,\n \"ftPercent\": 0.833,\n \"offensiveRb\": 8,\n \"defensiveRb\": 68,\n \"totalRb\": 76,\n \"assists\": 106,\n \"steals\": 27,\n \"blocks\": 5,\n \"turnovers\": 60,\n \"personalFouls\": 40,\n \"points\": 430,\n \"team\": \"POR\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:41.632205Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:41.632205Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7395,\n \"id\": 116,\n \"playerId\": \"leonaka01\",\n \"playerName\": \"Kawhi Leonard\",\n \"position\": \"SF\",\n \"age\": 27,\n \"games\": 24,\n \"gamesStarted\": 24,\n \"minutesPg\": 939,\n \"fieldGoals\": 243,\n \"fieldAttempts\": 496,\n \"fieldPercent\": 0.49,\n \"threeFg\": 55,\n \"threeAttempts\": 145,\n \"threePercent\": 0.379,\n \"twoFg\": 188,\n \"twoAttempts\": 351,\n \"twoPercent\": 0.536,\n \"effectFgPercent\": 0.545,\n \"ft\": 191,\n \"ftAttempts\": 216,\n \"ftPercent\": 0.884,\n \"offensiveRb\": 54,\n \"defensiveRb\": 164,\n \"totalRb\": 218,\n \"assists\": 94,\n \"steals\": 40,\n \"blocks\": 17,\n \"turnovers\": 74,\n \"personalFouls\": 56,\n \"points\": 732,\n \"team\": \"TOR\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:40.895323Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:40.895323Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7376,\n \"id\": 97,\n \"playerId\": \"iguodan01\",\n \"playerName\": \"Andre Iguodala\",\n \"position\": \"SF\",\n \"age\": 35,\n \"games\": 21,\n \"gamesStarted\": 15,\n \"minutesPg\": 629,\n \"fieldGoals\": 82,\n \"fieldAttempts\": 166,\n \"fieldPercent\": 0.494,\n \"threeFg\": 28,\n \"threeAttempts\": 80,\n \"threePercent\": 0.35,\n \"twoFg\": 54,\n \"twoAttempts\": 86,\n \"twoPercent\": 0.628,\n \"effectFgPercent\": 0.578,\n \"ft\": 14,\n \"ftAttempts\": 37,\n \"ftPercent\": 0.378,\n \"offensiveRb\": 27,\n \"defensiveRb\": 64,\n \"totalRb\": 91,\n \"assists\": 83,\n \"steals\": 24,\n \"blocks\": 23,\n \"turnovers\": 21,\n \"personalFouls\": 43,\n \"points\": 206,\n \"team\": \"GSW\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:37.278972Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:37.278972Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7285,\n \"id\": 6,\n \"playerId\": \"antetgi01\",\n \"playerName\": \"Giannis Antetokounmpo\",\n \"position\": \"PF\",\n \"age\": 24,\n \"games\": 15,\n \"gamesStarted\": 15,\n \"minutesPg\": 514,\n \"fieldGoals\": 129,\n \"fieldAttempts\": 261,\n \"fieldPercent\": 0.494,\n \"threeFg\": 18,\n \"threeAttempts\": 55,\n \"threePercent\": 0.327,\n \"twoFg\": 111,\n \"twoAttempts\": 206,\n \"twoPercent\": 0.539,\n \"effectFgPercent\": 0.529,\n \"ft\": 107,\n \"ftAttempts\": 168,\n \"ftPercent\": 0.637,\n \"offensiveRb\": 36,\n \"defensiveRb\": 147,\n \"totalRb\": 183,\n \"assists\": 73,\n \"steals\": 18,\n \"blocks\": 30,\n \"turnovers\": 50,\n \"personalFouls\": 48,\n \"points\": 383,\n \"team\": \"MIL\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:19.819646Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:19.819646Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7344,\n \"id\": 65,\n \"playerId\": \"gasolma01\",\n \"playerName\": \"Marc Gasol\",\n \"position\": \"C\",\n \"age\": 34,\n \"games\": 24,\n \"gamesStarted\": 24,\n \"minutesPg\": 735,\n \"fieldGoals\": 76,\n \"fieldAttempts\": 180,\n \"fieldPercent\": 0.422,\n \"threeFg\": 34,\n \"threeAttempts\": 89,\n \"threePercent\": 0.382,\n \"twoFg\": 42,\n \"twoAttempts\": 91,\n \"twoPercent\": 0.462,\n \"effectFgPercent\": 0.517,\n \"ft\": 40,\n \"ftAttempts\": 46,\n \"ftPercent\": 0.87,\n \"offensiveRb\": 16,\n \"defensiveRb\": 138,\n \"totalRb\": 154,\n \"assists\": 73,\n \"steals\": 21,\n \"blocks\": 26,\n \"turnovers\": 21,\n \"personalFouls\": 84,\n \"points\": 226,\n \"team\": \"TOR\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:31.095854Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:31.095854Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7359,\n \"id\": 80,\n \"playerId\": \"hardeja01\",\n \"playerName\": \"James Harden\",\n \"position\": \"PG\",\n \"age\": 29,\n \"games\": 11,\n \"gamesStarted\": 11,\n \"minutesPg\": 424,\n \"fieldGoals\": 109,\n \"fieldAttempts\": 264,\n \"fieldPercent\": 0.413,\n \"threeFg\": 48,\n \"threeAttempts\": 137,\n \"threePercent\": 0.35,\n \"twoFg\": 61,\n \"twoAttempts\": 127,\n \"twoPercent\": 0.48,\n \"effectFgPercent\": 0.504,\n \"ft\": 82,\n \"ftAttempts\": 98,\n \"ftPercent\": 0.837,\n \"offensiveRb\": 9,\n \"defensiveRb\": 66,\n \"totalRb\": 75,\n \"assists\": 73,\n \"steals\": 24,\n \"blocks\": 10,\n \"turnovers\": 51,\n \"personalFouls\": 41,\n \"points\": 348,\n \"team\": \"HOU\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:33.976424Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:33.976424Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7460,\n \"id\": 181,\n \"playerId\": \"simmobe01\",\n \"playerName\": \"Ben Simmons\",\n \"position\": \"PG\",\n \"age\": 22,\n \"games\": 12,\n \"gamesStarted\": 12,\n \"minutesPg\": 421,\n \"fieldGoals\": 72,\n \"fieldAttempts\": 116,\n \"fieldPercent\": 0.621,\n \"threeFg\": 0,\n \"threeAttempts\": 0,\n \"threePercent\": 0,\n \"twoFg\": 72,\n \"twoAttempts\": 116,\n \"twoPercent\": 0.621,\n \"effectFgPercent\": 0.621,\n \"ft\": 23,\n \"ftAttempts\": 40,\n \"ftPercent\": 0.575,\n \"offensiveRb\": 23,\n \"defensiveRb\": 61,\n \"totalRb\": 84,\n \"assists\": 72,\n \"steals\": 15,\n \"blocks\": 12,\n \"turnovers\": 32,\n \"personalFouls\": 45,\n \"points\": 167,\n \"team\": \"PHI\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:53.424036Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:53.424036Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7416,\n \"id\": 137,\n \"playerId\": \"middlkh01\",\n \"playerName\": \"Khris Middleton\",\n \"position\": \"SF\",\n \"age\": 27,\n \"games\": 15,\n \"gamesStarted\": 15,\n \"minutesPg\": 515,\n \"fieldGoals\": 84,\n \"fieldAttempts\": 201,\n \"fieldPercent\": 0.418,\n \"threeFg\": 40,\n \"threeAttempts\": 92,\n \"threePercent\": 0.435,\n \"twoFg\": 44,\n \"twoAttempts\": 109,\n \"twoPercent\": 0.404,\n \"effectFgPercent\": 0.517,\n \"ft\": 46,\n \"ftAttempts\": 52,\n \"ftPercent\": 0.885,\n \"offensiveRb\": 9,\n \"defensiveRb\": 86,\n \"totalRb\": 95,\n \"assists\": 66,\n \"steals\": 9,\n \"blocks\": 0,\n \"turnovers\": 29,\n \"personalFouls\": 44,\n \"points\": 254,\n \"team\": \"MIL\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:44.932463Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:44.932463Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7428,\n \"id\": 149,\n \"playerId\": \"murraja01\",\n \"playerName\": \"Jamal Murray\",\n \"position\": \"PG\",\n \"age\": 21,\n \"games\": 14,\n \"gamesStarted\": 14,\n \"minutesPg\": 508,\n \"fieldGoals\": 107,\n \"fieldAttempts\": 252,\n \"fieldPercent\": 0.425,\n \"threeFg\": 28,\n \"threeAttempts\": 83,\n \"threePercent\": 0.337,\n \"twoFg\": 79,\n \"twoAttempts\": 169,\n \"twoPercent\": 0.467,\n \"effectFgPercent\": 0.48,\n \"ft\": 56,\n \"ftAttempts\": 62,\n \"ftPercent\": 0.903,\n \"offensiveRb\": 19,\n \"defensiveRb\": 42,\n \"totalRb\": 61,\n \"assists\": 66,\n \"steals\": 14,\n \"blocks\": 2,\n \"turnovers\": 22,\n \"personalFouls\": 24,\n \"points\": 298,\n \"team\": \"DEN\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:47.284827Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:47.284827Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7459,\n \"id\": 180,\n \"playerId\": \"siakapa01\",\n \"playerName\": \"Pascal Siakam\",\n \"position\": \"PF\",\n \"age\": 24,\n \"games\": 24,\n \"gamesStarted\": 24,\n \"minutesPg\": 891,\n \"fieldGoals\": 180,\n \"fieldAttempts\": 383,\n \"fieldPercent\": 0.47,\n \"threeFg\": 29,\n \"threeAttempts\": 104,\n \"threePercent\": 0.279,\n \"twoFg\": 151,\n \"twoAttempts\": 279,\n \"twoPercent\": 0.541,\n \"effectFgPercent\": 0.508,\n \"ft\": 66,\n \"ftAttempts\": 87,\n \"ftPercent\": 0.759,\n \"offensiveRb\": 40,\n \"defensiveRb\": 131,\n \"totalRb\": 171,\n \"assists\": 66,\n \"steals\": 24,\n \"blocks\": 17,\n \"turnovers\": 33,\n \"personalFouls\": 67,\n \"points\": 455,\n \"team\": \"TOR\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:53.175678Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:53.175678Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7295,\n \"id\": 16,\n \"playerId\": \"bledser01\",\n \"playerName\": \"Eric Bledsoe\",\n \"position\": \"PG\",\n \"age\": 29,\n \"games\": 15,\n \"gamesStarted\": 15,\n \"minutesPg\": 423,\n \"fieldGoals\": 76,\n \"fieldAttempts\": 185,\n \"fieldPercent\": 0.411,\n \"threeFg\": 17,\n \"threeAttempts\": 72,\n \"threePercent\": 0.236,\n \"twoFg\": 59,\n \"twoAttempts\": 113,\n \"twoPercent\": 0.522,\n \"effectFgPercent\": 0.457,\n \"ft\": 36,\n \"ftAttempts\": 51,\n \"ftPercent\": 0.706,\n \"offensiveRb\": 20,\n \"defensiveRb\": 36,\n \"totalRb\": 56,\n \"assists\": 64,\n \"steals\": 16,\n \"blocks\": 6,\n \"turnovers\": 32,\n \"personalFouls\": 28,\n \"points\": 205,\n \"team\": \"MIL\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:21.702841Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:21.702841Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7379,\n \"id\": 100,\n \"playerId\": \"irvinky01\",\n \"playerName\": \"Kyrie Irving\",\n \"position\": \"PG\",\n \"age\": 26,\n \"games\": 9,\n \"gamesStarted\": 9,\n \"minutesPg\": 330,\n \"fieldGoals\": 69,\n \"fieldAttempts\": 179,\n \"fieldPercent\": 0.385,\n \"threeFg\": 18,\n \"threeAttempts\": 58,\n \"threePercent\": 0.31,\n \"twoFg\": 51,\n \"twoAttempts\": 121,\n \"twoPercent\": 0.421,\n \"effectFgPercent\": 0.436,\n \"ft\": 36,\n \"ftAttempts\": 40,\n \"ftPercent\": 0.9,\n \"offensiveRb\": 5,\n \"defensiveRb\": 34,\n \"totalRb\": 39,\n \"assists\": 63,\n \"steals\": 13,\n \"blocks\": 4,\n \"turnovers\": 28,\n \"personalFouls\": 26,\n \"points\": 192,\n \"team\": \"BOS\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:37.814543Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:37.814543Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7305,\n \"id\": 26,\n \"playerId\": \"butleji01\",\n \"playerName\": \"Jimmy Butler\",\n \"position\": \"SF\",\n \"age\": 29,\n \"games\": 12,\n \"gamesStarted\": 12,\n \"minutesPg\": 421,\n \"fieldGoals\": 79,\n \"fieldAttempts\": 175,\n \"fieldPercent\": 0.451,\n \"threeFg\": 12,\n \"threeAttempts\": 45,\n \"threePercent\": 0.267,\n \"twoFg\": 67,\n \"twoAttempts\": 130,\n \"twoPercent\": 0.515,\n \"effectFgPercent\": 0.486,\n \"ft\": 63,\n \"ftAttempts\": 72,\n \"ftPercent\": 0.875,\n \"offensiveRb\": 22,\n \"defensiveRb\": 50,\n \"totalRb\": 72,\n \"assists\": 62,\n \"steals\": 18,\n \"blocks\": 7,\n \"turnovers\": 22,\n \"personalFouls\": 20,\n \"points\": 233,\n \"team\": \"PHI\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:23.708252Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:23.708252Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7479,\n \"id\": 200,\n \"playerId\": \"vanvlfr01\",\n \"playerName\": \"Fred VanVleet\",\n \"position\": \"PG\",\n \"age\": 24,\n \"games\": 24,\n \"gamesStarted\": 0,\n \"minutesPg\": 592,\n \"fieldGoals\": 65,\n \"fieldAttempts\": 166,\n \"fieldPercent\": 0.392,\n \"threeFg\": 38,\n \"threeAttempts\": 98,\n \"threePercent\": 0.388,\n \"twoFg\": 27,\n \"twoAttempts\": 68,\n \"twoPercent\": 0.397,\n \"effectFgPercent\": 0.506,\n \"ft\": 24,\n \"ftAttempts\": 31,\n \"ftPercent\": 0.774,\n \"offensiveRb\": 7,\n \"defensiveRb\": 33,\n \"totalRb\": 40,\n \"assists\": 62,\n \"steals\": 18,\n \"blocks\": 6,\n \"turnovers\": 21,\n \"personalFouls\": 49,\n \"points\": 192,\n \"team\": \"TOR\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:57.203361Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:57.203361Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7439,\n \"id\": 160,\n \"playerId\": \"paulch01\",\n \"playerName\": \"Chris Paul\",\n \"position\": \"PG\",\n \"age\": 33,\n \"games\": 11,\n \"gamesStarted\": 11,\n \"minutesPg\": 397,\n \"fieldGoals\": 66,\n \"fieldAttempts\": 148,\n \"fieldPercent\": 0.446,\n \"threeFg\": 17,\n \"threeAttempts\": 63,\n \"threePercent\": 0.27,\n \"twoFg\": 49,\n \"twoAttempts\": 85,\n \"twoPercent\": 0.576,\n \"effectFgPercent\": 0.503,\n \"ft\": 38,\n \"ftAttempts\": 45,\n \"ftPercent\": 0.844,\n \"offensiveRb\": 14,\n \"defensiveRb\": 56,\n \"totalRb\": 70,\n \"assists\": 60,\n \"steals\": 24,\n \"blocks\": 7,\n \"turnovers\": 37,\n \"personalFouls\": 36,\n \"points\": 187,\n \"team\": \"HOU\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:49.350696Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:49.350696Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 7411,\n \"id\": 132,\n \"playerId\": \"mccolcj01\",\n \"playerName\": \"CJ McCollum\",\n \"position\": \"SG\",\n \"age\": 27,\n \"games\": 16,\n \"gamesStarted\": 16,\n \"minutesPg\": 635,\n \"fieldGoals\": 154,\n \"fieldAttempts\": 350,\n \"fieldPercent\": 0.44,\n \"threeFg\": 46,\n \"threeAttempts\": 117,\n \"threePercent\": 0.393,\n \"twoFg\": 108,\n \"twoAttempts\": 233,\n \"twoPercent\": 0.464,\n \"effectFgPercent\": 0.506,\n \"ft\": 41,\n \"ftAttempts\": 56,\n \"ftPercent\": 0.732,\n \"offensiveRb\": 14,\n \"defensiveRb\": 66,\n \"totalRb\": 80,\n \"assists\": 59,\n \"steals\": 13,\n \"blocks\": 10,\n \"turnovers\": 30,\n \"personalFouls\": 34,\n \"points\": 395,\n \"team\": \"POR\",\n \"season\": 2019,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T02:54:43.98991Z\",\n \"UpdatedAt\": \"2025-06-20T02:54:43.98991Z\",\n \"DeletedAt\": null\n }\n ],\n \"pagination\": {\n \"page\": 1,\n \"pageSize\": 20,\n \"pages\": 11,\n \"total\": 212\n }\n}" + } + ] + }, + { + "name": "Player Advanced Stats", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "", + "pm.test(\"Response time is within acceptable range\", function () {", + " pm.expect(pm.response.responseTime).to.be.below(200);", + "});", + "", + "", + "pm.test(\"Response schema matches the expected structure\", function () {", + " const responseData = pm.response.json();", + " ", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.data).to.be.an('array').that.is.not.empty;", + " ", + " responseData.data.forEach(function(playerStats) {", + " pm.expect(playerStats).to.have.property('ID');", + " pm.expect(playerStats).to.have.property('CreatedAt');", + " pm.expect(playerStats).to.have.property('UpdatedAt');", + " pm.expect(playerStats).to.have.property('playerId');", + " pm.expect(playerStats).to.have.property('playerName');", + " pm.expect(playerStats).to.have.property('position');", + " pm.expect(playerStats).to.have.property('age');", + " pm.expect(playerStats).to.have.property('games');", + " pm.expect(playerStats).to.have.property('minutesPlayed');", + " pm.expect(playerStats).to.have.property('per');", + " pm.expect(playerStats).to.have.property('tsPercent');", + " pm.expect(playerStats).to.have.property('threePAR');", + " pm.expect(playerStats).to.have.property('ftr');", + " pm.expect(playerStats).to.have.property('offensiveRBPercent');", + " pm.expect(playerStats).to.have.property('defensiveRBPercent');", + " pm.expect(playerStats).to.have.property('totalRBPercent');", + " pm.expect(playerStats).to.have.property('assistPercent');", + " pm.expect(playerStats).to.have.property('stealPercent');", + " pm.expect(playerStats).to.have.property('blockPercent');", + " pm.expect(playerStats).to.have.property('turnoverPercent');", + " pm.expect(playerStats).to.have.property('usagePercent');", + " pm.expect(playerStats).to.have.property('offensiveWS');", + " pm.expect(playerStats).to.have.property('defensiveWS');", + " pm.expect(playerStats).to.have.property('winShares');", + " pm.expect(playerStats).to.have.property('winSharesPer');", + " pm.expect(playerStats).to.have.property('offensiveBox');", + " pm.expect(playerStats).to.have.property('defensiveBox');", + " pm.expect(playerStats).to.have.property('box');", + " pm.expect(playerStats).to.have.property('vorp');", + " pm.expect(playerStats).to.have.property('team');", + " pm.expect(playerStats).to.have.property('season');", + " });", + "});", + "pm.test(\"Response time is within acceptable range\", function () {", + " pm.expect(pm.response.responseTime).to.be.below(200);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{api-key}}", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{base_url}}/api/playeradvancedstats?season=2025&team=HOU&page=1&pageSize=35&sortBy=vorp&ascending=false&isPlayoff=false", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "playeradvancedstats" + ], + "query": [ + { + "key": "season", + "value": "2025", + "description": "(Optional) The NBA season year (e.g., 2025 for the 2024-25 season)." + }, + { + "key": "team", + "value": "HOU", + "description": "(Optional) The three-letter team abbreviation (e.g., LAL, HOU, MIL)." + }, + { + "key": "page", + "value": "1", + "description": "(Optional) The page number for pagination. Defaults to 1." + }, + { + "key": "pageSize", + "value": "35", + "description": "(Optional) The number of results to return per page. Defaults to 20." + }, + { + "key": "sortBy", + "value": "vorp", + "description": "(Optional) The field to sort the results by. Defaults to 'winShares'." + }, + { + "key": "ascending", + "value": "false", + "description": "(Optional) If true, sorts the results in ascending order. Defaults to false (descending)." + }, + { + "key": "isPlayoff", + "value": "false", + "description": "(Optional) Set to 'true' to retrieve playoff stats, or 'false' for regular season stats." + } + ] + }, + "description": "### Get Player Advanced Stats\n\nReturns a paginated and filterable list of advanced player statistics for a given season. You can retrieve either regular season or playoff data.\n\n### Usage\n\nThis endpoint supports filtering by `season`, `team`, and `playerId`. Results can be paginated using `page` and `pageSize`, and sorted using `sortBy` and `ascending`.\n\n#### **Query Parameters**\n\n* **`season`** (integer, optional): The season year (e.g., 2025).\n* **`team`** (string, optional): Team abbreviation (e.g., MIL).\n* **`playerId`** (string, optional): The player's unique ID (e.g., `greenaj01`).\n* **`page`** (integer, optional): Page number. Defaults to `1`.\n* **`pageSize`** (integer, optional): Number of results per page. Defaults to `20`.\n* **`isPlayoff`** (boolean, optional): Set to `true` for playoff stats, `false` for regular season. The filter is only applied if the parameter is provided.\n* **`sortBy`** (string, optional): Field to sort by. Defaults to `winShares`.\n* **`ascending`** (boolean, optional): Use `true` for ascending order. Defaults to `false`.\n\n#### **Available `sortBy` Fields:**\n`playerId`, `playerName`, `position`, `age`, `games`, `minutesPlayed`, `per`, `tsPercent`, `threePAR`, `ftr`, `offensiveRBPercent`, `defensiveRBPercent`, `totalRBPercent`, `assistPercent`, `stealPercent`, `blockPercent`, `turnoverPercent`, `usagePercent`, `offensiveWS`, `defensiveWS`, `winShares`, `winSharesPer`, `offensiveBox`, `defensiveBox`, `box`, `vorp`, `team`, `season`" + }, + "response": [ + { + "name": "Player Advanced Stats", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{api-key}}", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{base_url}}/api/playeradvancedstats?season=2025&team=HOU&page=1&pageSize=35&sortBy=vorp&ascending=false&isPlayoff=false", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "playeradvancedstats" + ], + "query": [ + { + "key": "season", + "value": "2025", + "description": "(Optional) The NBA season year (e.g., 2025 for the 2024-25 season)." + }, + { + "key": "team", + "value": "HOU", + "description": "(Optional) The three-letter team abbreviation (e.g., LAL, HOU, MIL)." + }, + { + "key": "page", + "value": "1", + "description": "(Optional) The page number for pagination. Defaults to 1." + }, + { + "key": "pageSize", + "value": "35", + "description": "(Optional) The number of results to return per page. Defaults to 20." + }, + { + "key": "sortBy", + "value": "vorp", + "description": "(Optional) The field to sort the results by. Defaults to 'winShares'." + }, + { + "key": "ascending", + "value": "false", + "description": "(Optional) If true, sorts the results in ascending order. Defaults to false (descending)." + }, + { + "key": "isPlayoff", + "value": "false", + "description": "(Optional) Set to 'true' to retrieve playoff stats, or 'false' for regular season stats." + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Alt-Svc", + "value": "h3=\":443\"; ma=2592000" + }, + { + "key": "Content-Encoding", + "value": "br" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Date", + "value": "Wed, 02 Jul 2025 01:18:43 GMT" + }, + { + "key": "Server", + "value": "nginx/1.28.0" + }, + { + "key": "Vary", + "value": "Accept-Encoding" + }, + { + "key": "Transfer-Encoding", + "value": "chunked" + } + ], + "cookie": [], + "body": "{\n \"data\": [\n {\n \"ID\": 1451,\n \"id\": 35,\n \"playerId\": \"sengual01\",\n \"playerName\": \"Alperen Şengün\",\n \"position\": \"C\",\n \"age\": 22,\n \"games\": 76,\n \"minutesPlayed\": 2395,\n \"per\": 21.4,\n \"tsPercent\": 0.545,\n \"threePAR\": 0.079,\n \"ftr\": 0.374,\n \"offensiveRBPercent\": 11.4,\n \"defensiveRBPercent\": 23.8,\n \"totalRBPercent\": 17.5,\n \"assistPercent\": 24.1,\n \"stealPercent\": 1.7,\n \"blockPercent\": 2.3,\n \"turnoverPercent\": 12.7,\n \"usagePercent\": 26.2,\n \"offensiveWS\": 4.1,\n \"defensiveWS\": 4.2,\n \"winShares\": 8.3,\n \"winSharesPer\": 0.166,\n \"offensiveBox\": 2.6,\n \"defensiveBox\": 1.8,\n \"box\": 4.4,\n \"vorp\": 3.9,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T00:59:56.345088Z\",\n \"UpdatedAt\": \"2025-06-20T00:59:56.345088Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1475,\n \"id\": 57,\n \"playerId\": \"thompam01\",\n \"playerName\": \"Amen Thompson\",\n \"position\": \"SF\",\n \"age\": 22,\n \"games\": 69,\n \"minutesPlayed\": 2225,\n \"per\": 18.7,\n \"tsPercent\": 0.602,\n \"threePAR\": 0.131,\n \"ftr\": 0.354,\n \"offensiveRBPercent\": 9,\n \"defensiveRBPercent\": 18.2,\n \"totalRBPercent\": 13.5,\n \"assistPercent\": 16.9,\n \"stealPercent\": 2.1,\n \"blockPercent\": 3.6,\n \"turnoverPercent\": 14.6,\n \"usagePercent\": 17.5,\n \"offensiveWS\": 4.1,\n \"defensiveWS\": 3.9,\n \"winShares\": 8,\n \"winSharesPer\": 0.172,\n \"offensiveBox\": 1.4,\n \"defensiveBox\": 2.6,\n \"box\": 4.1,\n \"vorp\": 3.4,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T00:59:59.812013Z\",\n \"UpdatedAt\": \"2025-06-20T00:59:59.812013Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1643,\n \"id\": 190,\n \"playerId\": \"easonta01\",\n \"playerName\": \"Tari Eason\",\n \"position\": \"PF\",\n \"age\": 23,\n \"games\": 57,\n \"minutesPlayed\": 1420,\n \"per\": 18.5,\n \"tsPercent\": 0.567,\n \"threePAR\": 0.329,\n \"ftr\": 0.186,\n \"offensiveRBPercent\": 9.4,\n \"defensiveRBPercent\": 17.9,\n \"totalRBPercent\": 13.6,\n \"assistPercent\": 8.5,\n \"stealPercent\": 3.3,\n \"blockPercent\": 3.2,\n \"turnoverPercent\": 9.7,\n \"usagePercent\": 19.4,\n \"offensiveWS\": 2.1,\n \"defensiveWS\": 2.9,\n \"winShares\": 5,\n \"winSharesPer\": 0.169,\n \"offensiveBox\": 1,\n \"defensiveBox\": 2.6,\n \"box\": 3.6,\n \"vorp\": 2,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:00:25.170944Z\",\n \"UpdatedAt\": \"2025-06-20T01:00:25.170944Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1424,\n \"id\": 10,\n \"playerId\": \"greenja05\",\n \"playerName\": \"Jalen Green\",\n \"position\": \"SG\",\n \"age\": 22,\n \"games\": 82,\n \"minutesPlayed\": 2697,\n \"per\": 15.1,\n \"tsPercent\": 0.544,\n \"threePAR\": 0.46,\n \"ftr\": 0.234,\n \"offensiveRBPercent\": 1.7,\n \"defensiveRBPercent\": 13.4,\n \"totalRBPercent\": 7.4,\n \"assistPercent\": 16,\n \"stealPercent\": 1.3,\n \"blockPercent\": 0.9,\n \"turnoverPercent\": 11.4,\n \"usagePercent\": 27.3,\n \"offensiveWS\": 1.9,\n \"defensiveWS\": 3.2,\n \"winShares\": 5.1,\n \"winSharesPer\": 0.092,\n \"offensiveBox\": 1.1,\n \"defensiveBox\": -0.5,\n \"box\": 0.5,\n \"vorp\": 1.7,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T00:59:52.368605Z\",\n \"UpdatedAt\": \"2025-06-20T00:59:52.368605Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1494,\n \"id\": 74,\n \"playerId\": \"vanvlfr01\",\n \"playerName\": \"Fred VanVleet\",\n \"position\": \"PG\",\n \"age\": 30,\n \"games\": 60,\n \"minutesPlayed\": 2111,\n \"per\": 12.8,\n \"tsPercent\": 0.515,\n \"threePAR\": 0.607,\n \"ftr\": 0.181,\n \"offensiveRBPercent\": 1.6,\n \"defensiveRBPercent\": 9.7,\n \"totalRBPercent\": 5.6,\n \"assistPercent\": 21.2,\n \"stealPercent\": 2.2,\n \"blockPercent\": 1.1,\n \"turnoverPercent\": 9.6,\n \"usagePercent\": 17.7,\n \"offensiveWS\": 2.4,\n \"defensiveWS\": 2.8,\n \"winShares\": 5.2,\n \"winSharesPer\": 0.119,\n \"offensiveBox\": -0.4,\n \"defensiveBox\": 1.3,\n \"box\": 0.9,\n \"vorp\": 1.5,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:00:02.726841Z\",\n \"UpdatedAt\": \"2025-06-20T01:00:02.726841Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1583,\n \"id\": 144,\n \"playerId\": \"smithja05\",\n \"playerName\": \"Jabari Smith Jr.\",\n \"position\": \"PF\",\n \"age\": 21,\n \"games\": 57,\n \"minutesPlayed\": 1716,\n \"per\": 12.6,\n \"tsPercent\": 0.562,\n \"threePAR\": 0.489,\n \"ftr\": 0.223,\n \"offensiveRBPercent\": 6.4,\n \"defensiveRBPercent\": 18.6,\n \"totalRBPercent\": 12.4,\n \"assistPercent\": 4.7,\n \"stealPercent\": 0.7,\n \"blockPercent\": 2.1,\n \"turnoverPercent\": 8.9,\n \"usagePercent\": 16.4,\n \"offensiveWS\": 1.9,\n \"defensiveWS\": 2.2,\n \"winShares\": 4.2,\n \"winSharesPer\": 0.117,\n \"offensiveBox\": -0.8,\n \"defensiveBox\": 0.1,\n \"box\": -0.7,\n \"vorp\": 0.6,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:00:16.318994Z\",\n \"UpdatedAt\": \"2025-06-20T01:00:16.318994Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1792,\n \"id\": 301,\n \"playerId\": \"whitmca01\",\n \"playerName\": \"Cam Whitmore\",\n \"position\": \"SF\",\n \"age\": 20,\n \"games\": 51,\n \"minutesPlayed\": 827,\n \"per\": 15.5,\n \"tsPercent\": 0.549,\n \"threePAR\": 0.454,\n \"ftr\": 0.179,\n \"offensiveRBPercent\": 4.5,\n \"defensiveRBPercent\": 15.1,\n \"totalRBPercent\": 9.7,\n \"assistPercent\": 8.9,\n \"stealPercent\": 1.8,\n \"blockPercent\": 1.4,\n \"turnoverPercent\": 9.6,\n \"usagePercent\": 23.9,\n \"offensiveWS\": 0.6,\n \"defensiveWS\": 1.2,\n \"winShares\": 1.8,\n \"winSharesPer\": 0.106,\n \"offensiveBox\": 0.7,\n \"defensiveBox\": -0.4,\n \"box\": 0.3,\n \"vorp\": 0.5,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:00:46.875219Z\",\n \"UpdatedAt\": \"2025-06-20T01:00:46.875219Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1795,\n \"id\": 304,\n \"playerId\": \"adamsst01\",\n \"playerName\": \"Steven Adams\",\n \"position\": \"C\",\n \"age\": 31,\n \"games\": 58,\n \"minutesPlayed\": 794,\n \"per\": 16.6,\n \"tsPercent\": 0.541,\n \"threePAR\": 0.012,\n \"ftr\": 0.557,\n \"offensiveRBPercent\": 21.8,\n \"defensiveRBPercent\": 22,\n \"totalRBPercent\": 21.9,\n \"assistPercent\": 10.9,\n \"stealPercent\": 1.3,\n \"blockPercent\": 3.2,\n \"turnoverPercent\": 20.6,\n \"usagePercent\": 13.6,\n \"offensiveWS\": 1,\n \"defensiveWS\": 1.3,\n \"winShares\": 2.3,\n \"winSharesPer\": 0.137,\n \"offensiveBox\": 0,\n \"defensiveBox\": 0.3,\n \"box\": 0.3,\n \"vorp\": 0.5,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:00:47.313648Z\",\n \"UpdatedAt\": \"2025-06-20T01:00:47.313648Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1452,\n \"id\": 36,\n \"playerId\": \"brookdi01\",\n \"playerName\": \"Dillon Brooks\",\n \"position\": \"SF\",\n \"age\": 29,\n \"games\": 75,\n \"minutesPlayed\": 2388,\n \"per\": 11,\n \"tsPercent\": 0.555,\n \"threePAR\": 0.524,\n \"ftr\": 0.135,\n \"offensiveRBPercent\": 3.3,\n \"defensiveRBPercent\": 9.1,\n \"totalRBPercent\": 6.1,\n \"assistPercent\": 7.4,\n \"stealPercent\": 1.2,\n \"blockPercent\": 0.6,\n \"turnoverPercent\": 7.4,\n \"usagePercent\": 17.7,\n \"offensiveWS\": 2.7,\n \"defensiveWS\": 2.5,\n \"winShares\": 5.1,\n \"winSharesPer\": 0.103,\n \"offensiveBox\": -1.4,\n \"defensiveBox\": 0,\n \"box\": -1.4,\n \"vorp\": 0.4,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T00:59:56.492538Z\",\n \"UpdatedAt\": \"2025-06-20T00:59:56.492538Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1796,\n \"id\": 305,\n \"playerId\": \"holidaa01\",\n \"playerName\": \"Aaron Holiday\",\n \"position\": \"PG\",\n \"age\": 28,\n \"games\": 62,\n \"minutesPlayed\": 792,\n \"per\": 12.2,\n \"tsPercent\": 0.594,\n \"threePAR\": 0.675,\n \"ftr\": 0.153,\n \"offensiveRBPercent\": 1.7,\n \"defensiveRBPercent\": 8.9,\n \"totalRBPercent\": 5.2,\n \"assistPercent\": 14.3,\n \"stealPercent\": 1.2,\n \"blockPercent\": 1.2,\n \"turnoverPercent\": 11.5,\n \"usagePercent\": 16.8,\n \"offensiveWS\": 1.2,\n \"defensiveWS\": 0.8,\n \"winShares\": 2,\n \"winSharesPer\": 0.122,\n \"offensiveBox\": -0.4,\n \"defensiveBox\": 0.1,\n \"box\": -0.2,\n \"vorp\": 0.3,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:00:47.482453Z\",\n \"UpdatedAt\": \"2025-06-20T01:00:47.482453Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1888,\n \"id\": 375,\n \"playerId\": \"landajo01\",\n \"playerName\": \"Jock Landale\",\n \"position\": \"C\",\n \"age\": 29,\n \"games\": 42,\n \"minutesPlayed\": 500,\n \"per\": 16,\n \"tsPercent\": 0.59,\n \"threePAR\": 0.171,\n \"ftr\": 0.263,\n \"offensiveRBPercent\": 11.7,\n \"defensiveRBPercent\": 17.6,\n \"totalRBPercent\": 14.6,\n \"assistPercent\": 10.3,\n \"stealPercent\": 1.3,\n \"blockPercent\": 1.8,\n \"turnoverPercent\": 11,\n \"usagePercent\": 15.7,\n \"offensiveWS\": 1,\n \"defensiveWS\": 0.7,\n \"winShares\": 1.7,\n \"winSharesPer\": 0.159,\n \"offensiveBox\": 0,\n \"defensiveBox\": 0.4,\n \"box\": 0.4,\n \"vorp\": 0.3,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:01:00.821492Z\",\n \"UpdatedAt\": \"2025-06-20T01:01:00.821492Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1862,\n \"id\": 353,\n \"playerId\": \"tateja01\",\n \"playerName\": \"Jae'Sean Tate\",\n \"position\": \"SF\",\n \"age\": 29,\n \"games\": 52,\n \"minutesPlayed\": 588,\n \"per\": 12.3,\n \"tsPercent\": 0.557,\n \"threePAR\": 0.311,\n \"ftr\": 0.318,\n \"offensiveRBPercent\": 8.9,\n \"defensiveRBPercent\": 12.4,\n \"totalRBPercent\": 10.6,\n \"assistPercent\": 10.5,\n \"stealPercent\": 2.2,\n \"blockPercent\": 1.1,\n \"turnoverPercent\": 11.5,\n \"usagePercent\": 13.3,\n \"offensiveWS\": 0.8,\n \"defensiveWS\": 0.8,\n \"winShares\": 1.6,\n \"winSharesPer\": 0.133,\n \"offensiveBox\": -2.1,\n \"defensiveBox\": 1.4,\n \"box\": -0.7,\n \"vorp\": 0.2,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:00:57.110788Z\",\n \"UpdatedAt\": \"2025-06-20T01:00:57.110788Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1931,\n \"id\": 404,\n \"playerId\": \"greenje02\",\n \"playerName\": \"Jeff Green\",\n \"position\": \"PF\",\n \"age\": 38,\n \"games\": 32,\n \"minutesPlayed\": 396,\n \"per\": 13.6,\n \"tsPercent\": 0.649,\n \"threePAR\": 0.653,\n \"ftr\": 0.215,\n \"offensiveRBPercent\": 2.9,\n \"defensiveRBPercent\": 12.9,\n \"totalRBPercent\": 7.8,\n \"assistPercent\": 7,\n \"stealPercent\": 0.7,\n \"blockPercent\": 0.9,\n \"turnoverPercent\": 6.4,\n \"usagePercent\": 14.7,\n \"offensiveWS\": 0.9,\n \"defensiveWS\": 0.4,\n \"winShares\": 1.3,\n \"winSharesPer\": 0.159,\n \"offensiveBox\": 0.3,\n \"defensiveBox\": 0,\n \"box\": 0.4,\n \"vorp\": 0.2,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:01:07.235443Z\",\n \"UpdatedAt\": \"2025-06-20T01:01:07.235443Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1837,\n \"id\": 336,\n \"playerId\": \"sheppre01\",\n \"playerName\": \"Reed Sheppard\",\n \"position\": \"PG\",\n \"age\": 20,\n \"games\": 52,\n \"minutesPlayed\": 654,\n \"per\": 9.7,\n \"tsPercent\": 0.465,\n \"threePAR\": 0.594,\n \"ftr\": 0.067,\n \"offensiveRBPercent\": 2.6,\n \"defensiveRBPercent\": 10.3,\n \"totalRBPercent\": 6.4,\n \"assistPercent\": 15.2,\n \"stealPercent\": 2.6,\n \"blockPercent\": 2.3,\n \"turnoverPercent\": 13.1,\n \"usagePercent\": 17.8,\n \"offensiveWS\": -0.3,\n \"defensiveWS\": 1,\n \"winShares\": 0.8,\n \"winSharesPer\": 0.055,\n \"offensiveBox\": -3,\n \"defensiveBox\": 1.3,\n \"box\": -1.7,\n \"vorp\": 0.1,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:00:53.498684Z\",\n \"UpdatedAt\": \"2025-06-20T01:00:53.498684Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2101,\n \"id\": 523,\n \"playerId\": \"nfalyda01\",\n \"playerName\": \"N'Faly Dante\",\n \"position\": \"C\",\n \"age\": 23,\n \"games\": 4,\n \"minutesPlayed\": 51,\n \"per\": 24,\n \"tsPercent\": 0.789,\n \"threePAR\": 0,\n \"ftr\": 0.385,\n \"offensiveRBPercent\": 12.3,\n \"defensiveRBPercent\": 32,\n \"totalRBPercent\": 21.9,\n \"assistPercent\": 5.7,\n \"stealPercent\": 1,\n \"blockPercent\": 8.8,\n \"turnoverPercent\": 11.6,\n \"usagePercent\": 13.9,\n \"offensiveWS\": 0.2,\n \"defensiveWS\": 0.1,\n \"winShares\": 0.3,\n \"winSharesPer\": 0.283,\n \"offensiveBox\": 0.7,\n \"defensiveBox\": 1.7,\n \"box\": 2.4,\n \"vorp\": 0.1,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:01:31.754493Z\",\n \"UpdatedAt\": \"2025-06-20T01:01:31.754493Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 1924,\n \"id\": 397,\n \"playerId\": \"roddyda01\",\n \"playerName\": \"David Roddy\",\n \"position\": \"PF\",\n \"age\": 23,\n \"games\": 3,\n \"minutesPlayed\": 35,\n \"per\": 7.2,\n \"tsPercent\": 0.44,\n \"threePAR\": 0.538,\n \"ftr\": 0.308,\n \"offensiveRBPercent\": 0,\n \"defensiveRBPercent\": 15.5,\n \"totalRBPercent\": 7.6,\n \"assistPercent\": 7.7,\n \"stealPercent\": 0,\n \"blockPercent\": 2.6,\n \"turnoverPercent\": 6.3,\n \"usagePercent\": 18.5,\n \"offensiveWS\": 0,\n \"defensiveWS\": 0,\n \"winShares\": 0,\n \"winSharesPer\": 0.007,\n \"offensiveBox\": -5.1,\n \"defensiveBox\": -3.2,\n \"box\": -8.3,\n \"vorp\": -0.1,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:01:06.249889Z\",\n \"UpdatedAt\": \"2025-06-20T01:01:06.249889Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2038,\n \"id\": 472,\n \"playerId\": \"willije02\",\n \"playerName\": \"Jeenathan Williams\",\n \"position\": \"SG\",\n \"age\": 25,\n \"games\": 20,\n \"minutesPlayed\": 147,\n \"per\": 8.6,\n \"tsPercent\": 0.496,\n \"threePAR\": 0.419,\n \"ftr\": 0.129,\n \"offensiveRBPercent\": 3.6,\n \"defensiveRBPercent\": 5.9,\n \"totalRBPercent\": 4.7,\n \"assistPercent\": 8.8,\n \"stealPercent\": 2.6,\n \"blockPercent\": 2.4,\n \"turnoverPercent\": 17.6,\n \"usagePercent\": 22.3,\n \"offensiveWS\": -0.2,\n \"defensiveWS\": 0.2,\n \"winShares\": 0,\n \"winSharesPer\": -0.007,\n \"offensiveBox\": -5.5,\n \"defensiveBox\": -0.2,\n \"box\": -5.7,\n \"vorp\": -0.1,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:01:22.535279Z\",\n \"UpdatedAt\": \"2025-06-20T01:01:22.535279Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2106,\n \"id\": 528,\n \"playerId\": \"mcveija01\",\n \"playerName\": \"Jack McVeigh\",\n \"position\": \"PF\",\n \"age\": 28,\n \"games\": 9,\n \"minutesPlayed\": 43,\n \"per\": 4,\n \"tsPercent\": 0.412,\n \"threePAR\": 0.765,\n \"ftr\": 0,\n \"offensiveRBPercent\": 4.9,\n \"defensiveRBPercent\": 7.6,\n \"totalRBPercent\": 6.2,\n \"assistPercent\": 3,\n \"stealPercent\": 0,\n \"blockPercent\": 4.2,\n \"turnoverPercent\": 10.5,\n \"usagePercent\": 18.2,\n \"offensiveWS\": -0.1,\n \"defensiveWS\": 0,\n \"winShares\": 0,\n \"winSharesPer\": -0.031,\n \"offensiveBox\": -4.8,\n \"defensiveBox\": -3,\n \"box\": -7.7,\n \"vorp\": -0.1,\n \"team\": \"HOU\",\n \"season\": 2025,\n \"isPlayoff\": false,\n \"CreatedAt\": \"2025-06-20T01:01:32.499427Z\",\n \"UpdatedAt\": \"2025-06-20T01:01:32.499427Z\",\n \"DeletedAt\": null\n }\n ],\n \"pagination\": {\n \"total\": 18,\n \"page\": 1,\n \"pageSize\": 35,\n \"pages\": 1\n }\n}" + } + ] + }, + { + "name": "Player Advanced Playoff Stats", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "", + "pm.test(\"Response time is within acceptable range\", function () {", + " pm.expect(pm.response.responseTime).to.be.below(200);", + "});", + "", + "", + "pm.test(\"Response schema matches the expected structure\", function () {", + " const responseData = pm.response.json();", + " ", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.data).to.be.an('array').that.is.not.empty;", + " ", + " responseData.data.forEach(function(playerStats) {", + " pm.expect(playerStats).to.have.property('ID');", + " pm.expect(playerStats).to.have.property('CreatedAt');", + " pm.expect(playerStats).to.have.property('UpdatedAt');", + " pm.expect(playerStats).to.have.property('playerId');", + " pm.expect(playerStats).to.have.property('playerName');", + " pm.expect(playerStats).to.have.property('position');", + " pm.expect(playerStats).to.have.property('age');", + " pm.expect(playerStats).to.have.property('games');", + " pm.expect(playerStats).to.have.property('minutesPlayed');", + " pm.expect(playerStats).to.have.property('per');", + " pm.expect(playerStats).to.have.property('tsPercent');", + " pm.expect(playerStats).to.have.property('threePAR');", + " pm.expect(playerStats).to.have.property('ftr');", + " pm.expect(playerStats).to.have.property('offensiveRBPercent');", + " pm.expect(playerStats).to.have.property('defensiveRBPercent');", + " pm.expect(playerStats).to.have.property('totalRBPercent');", + " pm.expect(playerStats).to.have.property('assistPercent');", + " pm.expect(playerStats).to.have.property('stealPercent');", + " pm.expect(playerStats).to.have.property('blockPercent');", + " pm.expect(playerStats).to.have.property('turnoverPercent');", + " pm.expect(playerStats).to.have.property('usagePercent');", + " pm.expect(playerStats).to.have.property('offensiveWS');", + " pm.expect(playerStats).to.have.property('defensiveWS');", + " pm.expect(playerStats).to.have.property('winShares');", + " pm.expect(playerStats).to.have.property('winSharesPer');", + " pm.expect(playerStats).to.have.property('offensiveBox');", + " pm.expect(playerStats).to.have.property('defensiveBox');", + " pm.expect(playerStats).to.have.property('box');", + " pm.expect(playerStats).to.have.property('vorp');", + " pm.expect(playerStats).to.have.property('team');", + " pm.expect(playerStats).to.have.property('season');", + " });", + "});", + "pm.test(\"Response time is within acceptable range\", function () {", + " pm.expect(pm.response.responseTime).to.be.below(200);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{api-key}}", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{base_url}}/api/playeradvancedstats?season=2025&team=OKC&page=1&pageSize=35&sortBy=defensiveBox&ascending=false&isPlayoff=true", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "playeradvancedstats" + ], + "query": [ + { + "key": "season", + "value": "2025", + "description": "(Optional) The NBA season year (e.g., 2025 for the 2024-25 season)." + }, + { + "key": "team", + "value": "OKC", + "description": "(Optional) The three-letter team abbreviation (e.g., LAL, HOU, MIL)." + }, + { + "key": "page", + "value": "1", + "description": "(Optional) The page number for pagination. Defaults to 1." + }, + { + "key": "pageSize", + "value": "35", + "description": "(Optional) The number of results to return per page. Defaults to 20." + }, + { + "key": "sortBy", + "value": "defensiveBox", + "description": "(Optional) The field to sort the results by. Defaults to 'winShares'." + }, + { + "key": "ascending", + "value": "false", + "description": "(Optional) If true, sorts the results in ascending order. Defaults to false (descending)." + }, + { + "key": "isPlayoff", + "value": "true", + "description": "(Optional) Set to 'true' to retrieve playoff stats, or 'false' for regular season stats." + } + ] + }, + "description": "### Get Player Advanced Stats (Playoffs)\n\nReturns a paginated and filterable list of advanced player statistics for the playoffs. This is an example request that sets `isPlayoff=true`.\n\n### Usage\n\nThis endpoint supports filtering by `season`, `team`, and `playerId`. Results can be paginated using `page` and `pageSize`, and sorted using `sortBy` and `ascending`.\n\n#### **Query Parameters**\n\n* **`season`** (integer, optional): The season year (e.g., 2025).\n* **`team`** (string, optional): Team abbreviation (e.g., MIL).\n* **`playerId`** (string, optional): The player's unique ID (e.g., `greenaj01`).\n* **`page`** (integer, optional): Page number. Defaults to `1`.\n* **`pageSize`** (integer, optional): Number of results per page. Defaults to `20`.\n* **`isPlayoff`** (boolean, optional): Set to `true` for playoff stats, `false` for regular season. The filter is only applied if the parameter is provided.\n* **`sortBy`** (string, optional): Field to sort by. Defaults to `winShares`.\n* **`ascending`** (boolean, optional): Use `true` for ascending order. Defaults to `false`.\n\n#### **Available `sortBy` Fields:**\n`playerId`, `playerName`, `position`, `age`, `games`, `minutesPlayed`, `per`, `tsPercent`, `threePAR`, `ftr`, `offensiveRBPercent`, `defensiveRBPercent`, `totalRBPercent`, `assistPercent`, `stealPercent`, `blockPercent`, `turnoverPercent`, `usagePercent`, `offensiveWS`, `defensiveWS`, `winShares`, `winSharesPer`, `offensiveBox`, `defensiveBox`, `box`, `vorp`, `team`, `season`" + }, + "response": [ + { + "name": "Player Advanced Playoff Stats", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{api-key}}", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{base_url}}/api/playeradvancedstats?season=2025&team=OKC&page=1&pageSize=35&sortBy=defensiveBox&ascending=false&isPlayoff=true", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "playeradvancedstats" + ], + "query": [ + { + "key": "season", + "value": "2025", + "description": "(Optional) The NBA season year (e.g., 2025 for the 2024-25 season)." + }, + { + "key": "team", + "value": "OKC", + "description": "(Optional) The three-letter team abbreviation (e.g., LAL, HOU, MIL)." + }, + { + "key": "page", + "value": "1", + "description": "(Optional) The page number for pagination. Defaults to 1." + }, + { + "key": "pageSize", + "value": "35", + "description": "(Optional) The number of results to return per page. Defaults to 20." + }, + { + "key": "sortBy", + "value": "defensiveBox", + "description": "(Optional) The field to sort the results by. Defaults to 'winShares'." + }, + { + "key": "ascending", + "value": "false", + "description": "(Optional) If true, sorts the results in ascending order. Defaults to false (descending)." + }, + { + "key": "isPlayoff", + "value": "true", + "description": "(Optional) Set to 'true' to retrieve playoff stats, or 'false' for regular season stats." + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Alt-Svc", + "value": "h3=\":443\"; ma=2592000" + }, + { + "key": "Content-Encoding", + "value": "br" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Date", + "value": "Wed, 02 Jul 2025 01:18:52 GMT" + }, + { + "key": "Server", + "value": "nginx/1.28.0" + }, + { + "key": "Vary", + "value": "Accept-Encoding" + }, + { + "key": "Content-Length", + "value": "1692" + } + ], + "cookie": [], + "body": "{\n \"data\": [\n {\n \"ID\": 2612,\n \"id\": 32,\n \"playerId\": \"carusal01\",\n \"playerName\": \"Alex Caruso\",\n \"position\": \"SG\",\n \"age\": 30,\n \"games\": 23,\n \"minutesPlayed\": 562,\n \"per\": 15.1,\n \"tsPercent\": 0.598,\n \"threePAR\": 0.563,\n \"ftr\": 0.244,\n \"offensiveRBPercent\": 3.3,\n \"defensiveRBPercent\": 8.8,\n \"totalRBPercent\": 6,\n \"assistPercent\": 12.6,\n \"stealPercent\": 3.9,\n \"blockPercent\": 2.3,\n \"turnoverPercent\": 9.7,\n \"usagePercent\": 14.8,\n \"offensiveWS\": 0.9,\n \"defensiveWS\": 1.1,\n \"winShares\": 2,\n \"winSharesPer\": 0.169,\n \"offensiveBox\": 0.4,\n \"defensiveBox\": 4.7,\n \"box\": 5,\n \"vorp\": 1,\n \"team\": \"OKC\",\n \"season\": 2025,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T01:02:53.068472Z\",\n \"UpdatedAt\": \"2025-06-20T01:02:53.068472Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2794,\n \"id\": 214,\n \"playerId\": \"willija07\",\n \"playerName\": \"Jaylin Williams\",\n \"position\": \"PF\",\n \"age\": 22,\n \"games\": 17,\n \"minutesPlayed\": 141,\n \"per\": 9.7,\n \"tsPercent\": 0.565,\n \"threePAR\": 0.714,\n \"ftr\": 0.314,\n \"offensiveRBPercent\": 3.1,\n \"defensiveRBPercent\": 21.8,\n \"totalRBPercent\": 12.3,\n \"assistPercent\": 16.2,\n \"stealPercent\": 2.8,\n \"blockPercent\": 1.4,\n \"turnoverPercent\": 23.1,\n \"usagePercent\": 15.6,\n \"offensiveWS\": 0,\n \"defensiveWS\": 0.3,\n \"winShares\": 0.3,\n \"winSharesPer\": 0.091,\n \"offensiveBox\": -4,\n \"defensiveBox\": 4.4,\n \"box\": 0.4,\n \"vorp\": 0.1,\n \"team\": \"OKC\",\n \"season\": 2025,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T01:03:19.343959Z\",\n \"UpdatedAt\": \"2025-06-20T01:03:19.343959Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2683,\n \"id\": 103,\n \"playerId\": \"jonesdi01\",\n \"playerName\": \"Dillon Jones\",\n \"position\": \"SF\",\n \"age\": 23,\n \"games\": 10,\n \"minutesPlayed\": 46,\n \"per\": 19.2,\n \"tsPercent\": 0.968,\n \"threePAR\": 0.364,\n \"ftr\": 0.182,\n \"offensiveRBPercent\": 7,\n \"defensiveRBPercent\": 14.3,\n \"totalRBPercent\": 10.6,\n \"assistPercent\": 16.6,\n \"stealPercent\": 1.1,\n \"blockPercent\": 2.2,\n \"turnoverPercent\": 33.6,\n \"usagePercent\": 16.5,\n \"offensiveWS\": 0.1,\n \"defensiveWS\": 0.1,\n \"winShares\": 0.2,\n \"winSharesPer\": 0.179,\n \"offensiveBox\": 3.4,\n \"defensiveBox\": 4,\n \"box\": 7.5,\n \"vorp\": 0.1,\n \"team\": \"OKC\",\n \"season\": 2025,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T01:03:03.218018Z\",\n \"UpdatedAt\": \"2025-06-20T01:03:03.218018Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2784,\n \"id\": 204,\n \"playerId\": \"wallaca01\",\n \"playerName\": \"Cason Wallace\",\n \"position\": \"SG\",\n \"age\": 21,\n \"games\": 23,\n \"minutesPlayed\": 516,\n \"per\": 10.8,\n \"tsPercent\": 0.52,\n \"threePAR\": 0.521,\n \"ftr\": 0.076,\n \"offensiveRBPercent\": 4.8,\n \"defensiveRBPercent\": 8.1,\n \"totalRBPercent\": 6.4,\n \"assistPercent\": 12.4,\n \"stealPercent\": 3,\n \"blockPercent\": 1.9,\n \"turnoverPercent\": 11.5,\n \"usagePercent\": 11.4,\n \"offensiveWS\": 0.3,\n \"defensiveWS\": 0.8,\n \"winShares\": 1.2,\n \"winSharesPer\": 0.111,\n \"offensiveBox\": -1.7,\n \"defensiveBox\": 3.7,\n \"box\": 2,\n \"vorp\": 0.5,\n \"team\": \"OKC\",\n \"season\": 2025,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T01:03:17.911668Z\",\n \"UpdatedAt\": \"2025-06-20T01:03:17.911668Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2638,\n \"id\": 58,\n \"playerId\": \"gilgesh01\",\n \"playerName\": \"Shai Gilgeous-Alexander\",\n \"position\": \"PG\",\n \"age\": 26,\n \"games\": 23,\n \"minutesPlayed\": 851,\n \"per\": 25.3,\n \"tsPercent\": 0.574,\n \"threePAR\": 0.224,\n \"ftr\": 0.431,\n \"offensiveRBPercent\": 2.9,\n \"defensiveRBPercent\": 12.9,\n \"totalRBPercent\": 7.9,\n \"assistPercent\": 30.6,\n \"stealPercent\": 2.2,\n \"blockPercent\": 2.4,\n \"turnoverPercent\": 9.1,\n \"usagePercent\": 32.9,\n \"offensiveWS\": 2.6,\n \"defensiveWS\": 1.4,\n \"winShares\": 3.9,\n \"winSharesPer\": 0.222,\n \"offensiveBox\": 5.7,\n \"defensiveBox\": 2.6,\n \"box\": 8.3,\n \"vorp\": 2.2,\n \"team\": \"OKC\",\n \"season\": 2025,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T01:02:56.768423Z\",\n \"UpdatedAt\": \"2025-06-20T01:02:56.768423Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2654,\n \"id\": 74,\n \"playerId\": \"harteis01\",\n \"playerName\": \"Isaiah Hartenstein\",\n \"position\": \"C\",\n \"age\": 26,\n \"games\": 23,\n \"minutesPlayed\": 516,\n \"per\": 18.3,\n \"tsPercent\": 0.632,\n \"threePAR\": 0,\n \"ftr\": 0.224,\n \"offensiveRBPercent\": 13.2,\n \"defensiveRBPercent\": 23.4,\n \"totalRBPercent\": 18.2,\n \"assistPercent\": 14.3,\n \"stealPercent\": 1.8,\n \"blockPercent\": 2.3,\n \"turnoverPercent\": 15.5,\n \"usagePercent\": 14.3,\n \"offensiveWS\": 1.1,\n \"defensiveWS\": 1,\n \"winShares\": 2.1,\n \"winSharesPer\": 0.199,\n \"offensiveBox\": 0.9,\n \"defensiveBox\": 2.6,\n \"box\": 3.5,\n \"vorp\": 0.7,\n \"team\": \"OKC\",\n \"season\": 2025,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T01:02:59.067598Z\",\n \"UpdatedAt\": \"2025-06-20T01:02:59.067598Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2678,\n \"id\": 98,\n \"playerId\": \"joeis01\",\n \"playerName\": \"Isaiah Joe\",\n \"position\": \"SG\",\n \"age\": 25,\n \"games\": 21,\n \"minutesPlayed\": 211,\n \"per\": 17.4,\n \"tsPercent\": 0.676,\n \"threePAR\": 0.767,\n \"ftr\": 0.192,\n \"offensiveRBPercent\": 3.6,\n \"defensiveRBPercent\": 11.4,\n \"totalRBPercent\": 7.5,\n \"assistPercent\": 9.8,\n \"stealPercent\": 1.6,\n \"blockPercent\": 0.9,\n \"turnoverPercent\": 8.1,\n \"usagePercent\": 17.3,\n \"offensiveWS\": 0.6,\n \"defensiveWS\": 0.3,\n \"winShares\": 0.8,\n \"winSharesPer\": 0.192,\n \"offensiveBox\": 3.5,\n \"defensiveBox\": 2.1,\n \"box\": 5.6,\n \"vorp\": 0.4,\n \"team\": \"OKC\",\n \"season\": 2025,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T01:03:02.486429Z\",\n \"UpdatedAt\": \"2025-06-20T01:03:02.486429Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2793,\n \"id\": 213,\n \"playerId\": \"willija06\",\n \"playerName\": \"Jalen Williams\",\n \"position\": \"SG\",\n \"age\": 23,\n \"games\": 23,\n \"minutesPlayed\": 796,\n \"per\": 19.1,\n \"tsPercent\": 0.544,\n \"threePAR\": 0.29,\n \"ftr\": 0.323,\n \"offensiveRBPercent\": 3.2,\n \"defensiveRBPercent\": 14.2,\n \"totalRBPercent\": 8.7,\n \"assistPercent\": 22.3,\n \"stealPercent\": 2,\n \"blockPercent\": 1.1,\n \"turnoverPercent\": 8.3,\n \"usagePercent\": 26.3,\n \"offensiveWS\": 1.3,\n \"defensiveWS\": 1.2,\n \"winShares\": 2.5,\n \"winSharesPer\": 0.153,\n \"offensiveBox\": 2.5,\n \"defensiveBox\": 1.7,\n \"box\": 4.2,\n \"vorp\": 1.2,\n \"team\": \"OKC\",\n \"season\": 2025,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T01:03:19.204511Z\",\n \"UpdatedAt\": \"2025-06-20T01:03:19.204511Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2663,\n \"id\": 83,\n \"playerId\": \"holmgch01\",\n \"playerName\": \"Chet Holmgren\",\n \"position\": \"C\",\n \"age\": 22,\n \"games\": 23,\n \"minutesPlayed\": 686,\n \"per\": 18.1,\n \"tsPercent\": 0.565,\n \"threePAR\": 0.342,\n \"ftr\": 0.365,\n \"offensiveRBPercent\": 6.6,\n \"defensiveRBPercent\": 25.1,\n \"totalRBPercent\": 15.8,\n \"assistPercent\": 4.8,\n \"stealPercent\": 1.2,\n \"blockPercent\": 6.3,\n \"turnoverPercent\": 8.3,\n \"usagePercent\": 20.8,\n \"offensiveWS\": 0.9,\n \"defensiveWS\": 1.4,\n \"winShares\": 2.3,\n \"winSharesPer\": 0.161,\n \"offensiveBox\": 0.9,\n \"defensiveBox\": 1.5,\n \"box\": 2.4,\n \"vorp\": 0.8,\n \"team\": \"OKC\",\n \"season\": 2025,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T01:03:00.356697Z\",\n \"UpdatedAt\": \"2025-06-20T01:03:00.356697Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2627,\n \"id\": 47,\n \"playerId\": \"dortlu01\",\n \"playerName\": \"Luguentz Dort\",\n \"position\": \"SF\",\n \"age\": 25,\n \"games\": 23,\n \"minutesPlayed\": 666,\n \"per\": 8.1,\n \"tsPercent\": 0.524,\n \"threePAR\": 0.854,\n \"ftr\": 0.134,\n \"offensiveRBPercent\": 5.2,\n \"defensiveRBPercent\": 9.4,\n \"totalRBPercent\": 7.3,\n \"assistPercent\": 4,\n \"stealPercent\": 2.1,\n \"blockPercent\": 1.5,\n \"turnoverPercent\": 10.3,\n \"usagePercent\": 12.3,\n \"offensiveWS\": 0.2,\n \"defensiveWS\": 1,\n \"winShares\": 1.1,\n \"winSharesPer\": 0.081,\n \"offensiveBox\": -2.3,\n \"defensiveBox\": 1.4,\n \"box\": -0.9,\n \"vorp\": 0.2,\n \"team\": \"OKC\",\n \"season\": 2025,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T01:02:55.202425Z\",\n \"UpdatedAt\": \"2025-06-20T01:02:55.202425Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2623,\n \"id\": 43,\n \"playerId\": \"diengou01\",\n \"playerName\": \"Ousmane Dieng\",\n \"position\": \"C\",\n \"age\": 21,\n \"games\": 9,\n \"minutesPlayed\": 32,\n \"per\": 16.2,\n \"tsPercent\": 0.643,\n \"threePAR\": 0.7,\n \"ftr\": 0.2,\n \"offensiveRBPercent\": 0,\n \"defensiveRBPercent\": 13.7,\n \"totalRBPercent\": 6.8,\n \"assistPercent\": 12.9,\n \"stealPercent\": 0,\n \"blockPercent\": 3.1,\n \"turnoverPercent\": 0,\n \"usagePercent\": 14.4,\n \"offensiveWS\": 0.1,\n \"defensiveWS\": 0,\n \"winShares\": 0.1,\n \"winSharesPer\": 0.202,\n \"offensiveBox\": 4.9,\n \"defensiveBox\": 1.3,\n \"box\": 6.3,\n \"vorp\": 0.1,\n \"team\": \"OKC\",\n \"season\": 2025,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T01:02:54.622461Z\",\n \"UpdatedAt\": \"2025-06-20T01:02:54.622461Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2796,\n \"id\": 216,\n \"playerId\": \"willike04\",\n \"playerName\": \"Kenrich Williams\",\n \"position\": \"PF\",\n \"age\": 30,\n \"games\": 16,\n \"minutesPlayed\": 137,\n \"per\": 9.9,\n \"tsPercent\": 0.457,\n \"threePAR\": 0.5,\n \"ftr\": 0.15,\n \"offensiveRBPercent\": 6.3,\n \"defensiveRBPercent\": 21.6,\n \"totalRBPercent\": 13.9,\n \"assistPercent\": 7,\n \"stealPercent\": 2.5,\n \"blockPercent\": 0.7,\n \"turnoverPercent\": 14.1,\n \"usagePercent\": 15.4,\n \"offensiveWS\": -0.1,\n \"defensiveWS\": 0.3,\n \"winShares\": 0.1,\n \"winSharesPer\": 0.052,\n \"offensiveBox\": -2.6,\n \"defensiveBox\": 0.8,\n \"box\": -1.8,\n \"vorp\": 0,\n \"team\": \"OKC\",\n \"season\": 2025,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T01:03:19.635894Z\",\n \"UpdatedAt\": \"2025-06-20T01:03:19.635894Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2791,\n \"id\": 211,\n \"playerId\": \"wiggiaa01\",\n \"playerName\": \"Aaron Wiggins\",\n \"position\": \"SG\",\n \"age\": 26,\n \"games\": 22,\n \"minutesPlayed\": 303,\n \"per\": 11.8,\n \"tsPercent\": 0.522,\n \"threePAR\": 0.58,\n \"ftr\": 0.143,\n \"offensiveRBPercent\": 5,\n \"defensiveRBPercent\": 13,\n \"totalRBPercent\": 9,\n \"assistPercent\": 9.5,\n \"stealPercent\": 1.6,\n \"blockPercent\": 2,\n \"turnoverPercent\": 11.8,\n \"usagePercent\": 20.1,\n \"offensiveWS\": 0,\n \"defensiveWS\": 0.4,\n \"winShares\": 0.4,\n \"winSharesPer\": 0.071,\n \"offensiveBox\": -0.5,\n \"defensiveBox\": 0.6,\n \"box\": 0.1,\n \"vorp\": 0.2,\n \"team\": \"OKC\",\n \"season\": 2025,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T01:03:18.910441Z\",\n \"UpdatedAt\": \"2025-06-20T01:03:18.910441Z\",\n \"DeletedAt\": null\n },\n {\n \"ID\": 2715,\n \"id\": 135,\n \"playerId\": \"mitchaj01\",\n \"playerName\": \"Ajay Mitchell\",\n \"position\": \"SG\",\n \"age\": 22,\n \"games\": 12,\n \"minutesPlayed\": 84,\n \"per\": 12.2,\n \"tsPercent\": 0.551,\n \"threePAR\": 0.371,\n \"ftr\": 0.143,\n \"offensiveRBPercent\": 3.8,\n \"defensiveRBPercent\": 7.8,\n \"totalRBPercent\": 5.8,\n \"assistPercent\": 16.2,\n \"stealPercent\": 1.2,\n \"blockPercent\": 0,\n \"turnoverPercent\": 13.9,\n \"usagePercent\": 21.8,\n \"offensiveWS\": 0,\n \"defensiveWS\": 0.1,\n \"winShares\": 0.1,\n \"winSharesPer\": 0.067,\n \"offensiveBox\": -1.3,\n \"defensiveBox\": -0.1,\n \"box\": -1.4,\n \"vorp\": 0,\n \"team\": \"OKC\",\n \"season\": 2025,\n \"isPlayoff\": true,\n \"CreatedAt\": \"2025-06-20T01:03:07.867398Z\",\n \"UpdatedAt\": \"2025-06-20T01:03:07.867398Z\",\n \"DeletedAt\": null\n }\n ],\n \"pagination\": {\n \"total\": 14,\n \"page\": 1,\n \"pageSize\": 35,\n \"pages\": 1\n }\n}" + } + ] + }, + { + "name": "Shot Chart", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Test to check if the response time is less than 200ms", + "pm.test(\"Response time is less than 200ms\", function () {", + " pm.expect(pm.response.responseTime).to.be.below(200);", + "});", + "", + "pm.test(\"Response status code is 200\", function () {", + " pm.expect(pm.response.code).to.eql(200);", + "});", + "", + "", + "pm.test(\"Response is an array with at least one element\", function () {", + " const responseData = pm.response.json();", + " ", + " pm.expect(responseData).to.be.an('array').that.is.not.empty;", + "});", + "", + "", + "pm.test(\"Validate the schema of the objects in the array\", function () {", + " const responseData = pm.response.json();", + "", + " pm.expect(responseData).to.be.an('array').that.is.not.empty;", + "", + " responseData.forEach(function(item) {", + " pm.expect(item).to.be.an('object');", + " pm.expect(item).to.have.all.keys('id', 'playerId', 'season', 'date', 'qtr', 'timeRemaining', 'top', 'left', 'playerName', 'result', 'shotType', 'distanceFt', 'lead', 'teamScore', 'opponentTeamScore', 'opponent', 'team', 'ID', 'CreatedAt', 'UpdatedAt', 'DeletedAt');", + " ", + " pm.expect(item.id).to.be.a('number');", + " pm.expect(item.playerId).to.be.a('string');", + " pm.expect(item.season).to.be.a('number');", + " pm.expect(item.date).to.be.a('string');", + " pm.expect(item.qtr).to.be.a('string');", + " pm.expect(item.timeRemaining).to.be.a('string');", + " pm.expect(item.top).to.be.a('number');", + " pm.expect(item.left).to.be.a('number');", + " pm.expect(item.playerName).to.be.a('string');", + " pm.expect(item.result).to.be.a('boolean');", + " pm.expect(item.shotType).to.be.a('string');", + " pm.expect(item.distanceFt).to.be.a('number');", + " pm.expect(item.lead).to.be.a('boolean');", + " pm.expect(item.teamScore).to.be.a('number');", + " pm.expect(item.opponentTeamScore).to.be.a('number');", + " pm.expect(item.opponent).to.be.a('string');", + " pm.expect(item.team).to.be.a('string');", + " pm.expect(item.ID).to.be.a('number');", + " pm.expect(item.CreatedAt).to.be.a('string');", + " pm.expect(item.UpdatedAt).to.be.a('string');", + " pm.expect(item.DeletedAt).to.satisfy(val => val === null || typeof val === 'object');", + " });", + "});", + "", + "", + "pm.test(\"Player ID is a non-empty string\", function () {", + " const responseData = pm.response.json();", + " ", + " pm.expect(responseData).to.be.an('array').that.is.not.empty;", + " responseData.forEach(function(item) {", + " pm.expect(item.playerId).to.exist.and.to.be.a('string').and.to.have.lengthOf.at.least(1, \"Player ID should not be empty\");", + " });", + "});", + "", + "", + "pm.test(\"Season is a positive integer\", function () {", + " const responseData = pm.response.json();", + " ", + " pm.expect(responseData).to.be.an('array').that.is.not.empty;", + " responseData.forEach(function(item) {", + " pm.expect(item.season).to.be.a('number').and.to.be.above(0);", + " });", + "});", + "", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{api-key}}", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{base_url}}/api/playershotchart?page=1&playerId=hardeja01&date=Dec%2017%2C2018", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "playershotchart" + ], + "query": [ + { + "key": "page", + "value": "1", + "description": "(Optional) The page number for pagination. Defaults to 1. Each page returns a maximum of 50 results." + }, + { + "key": "playerId", + "value": "hardeja01", + "description": "(Optional) The unique ID for the player (e.g., hardeja01)." + }, + { + "key": "date", + "value": "Dec%2017%2C2018", + "description": "(Optional) The date of the game (e.g., 'Dec 17, 2018')." + }, + { + "key": "season", + "value": "2024", + "description": "(Optional) The season year (e.g., 2024 for the 2023-24 season).", + "disabled": true + } + ] + }, + "description": "### Get Player Shot Chart Data\n\nReturns a paginated list of shot chart data points for a player. The data can be filtered by a variety of parameters to narrow down the results.\n\n### Usage\n\nThis endpoint is ideal for visualizing player shooting performance. You can filter by a specific player, season, game date, quarter, and more.\n\n**Note:** Pagination is available via the `page` parameter, but the number of results per page is fixed at **50**.\n\n#### **Query Parameters**\n\n* **`page`** (integer, optional): The page number for pagination. Defaults to `1`.\n* **`playerId`** (string, optional): The player's unique ID (e.g., `hardeja01`).\n* **`season`** (integer, optional): The season year (e.g., `2023`).\n* **`date`** (string, optional): The specific game date (e.g., `Oct 17, 2018`).\n* **`qtr`** (string, optional): The quarter of the game (e.g., `1st Qtr`).\n* **`result`** (boolean, optional): Filter by shot outcome (`true` for made shots, `false` for missed).\n* **`shot_type`** (string, optional): The type of shot (e.g., `2-pointer`, `3-pointer`).\n* **`opponent`** (string, optional): The three-letter abbreviation for the opponent team (e.g., `NOP`)." + }, + "response": [ + { + "name": "Shot Chart", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{api-key}}", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{base_url}}/api/playershotchart?page=1&playerId=hardeja01&date=Dec%2017%2C2018", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "playershotchart" + ], + "query": [ + { + "key": "page", + "value": "1", + "description": "(Optional) The page number for pagination. Defaults to 1. Each page returns a maximum of 50 results." + }, + { + "key": "playerId", + "value": "hardeja01", + "description": "(Optional) The unique ID for the player (e.g., hardeja01)." + }, + { + "key": "date", + "value": "Dec%2017%2C2018", + "description": "(Optional) The date of the game (e.g., 'Dec 17, 2018')." + }, + { + "key": "season", + "value": "2024", + "description": "(Optional) The season year (e.g., 2024 for the 2023-24 season).", + "disabled": true + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Alt-Svc", + "value": "h3=\":443\"; ma=2592000" + }, + { + "key": "Content-Encoding", + "value": "br" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Date", + "value": "Wed, 02 Jul 2025 01:18:59 GMT" + }, + { + "key": "Server", + "value": "nginx/1.28.0" + }, + { + "key": "Vary", + "value": "Accept-Encoding" + }, + { + "key": "Content-Length", + "value": "813" + } + ], + "cookie": [], + "body": "[\n {\n \"id\": 510,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"11:09\",\n \"top\": 305,\n \"left\": 161,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 26,\n \"lead\": true,\n \"teamScore\": 3,\n \"opponentTeamScore\": 0,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 511,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"8:27\",\n \"top\": 130,\n \"left\": 291,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 9,\n \"lead\": true,\n \"teamScore\": 8,\n \"opponentTeamScore\": 4,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 512,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"7:54\",\n \"top\": 130,\n \"left\": 263,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 8,\n \"lead\": true,\n \"teamScore\": 10,\n \"opponentTeamScore\": 6,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 513,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"5:45\",\n \"top\": 218,\n \"left\": 53,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": true,\n \"teamScore\": 16,\n \"opponentTeamScore\": 12,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 514,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"4:25\",\n \"top\": 323,\n \"left\": 202,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 16,\n \"opponentTeamScore\": 15,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 515,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"4:18\",\n \"top\": 68,\n \"left\": 220,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 3,\n \"lead\": true,\n \"teamScore\": 18,\n \"opponentTeamScore\": 15,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 516,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"3:50\",\n \"top\": 269,\n \"left\": 362,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": true,\n \"teamScore\": 18,\n \"opponentTeamScore\": 17,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 517,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"3:15\",\n \"top\": 64,\n \"left\": 233,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 1,\n \"lead\": true,\n \"teamScore\": 20,\n \"opponentTeamScore\": 19,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 518,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"2:42\",\n \"top\": 56,\n \"left\": 232,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 1,\n \"lead\": true,\n \"teamScore\": 22,\n \"opponentTeamScore\": 19,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 519,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"8:05\",\n \"top\": 59,\n \"left\": 222,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 2,\n \"lead\": true,\n \"teamScore\": 32,\n \"opponentTeamScore\": 23,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 520,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"5:37\",\n \"top\": 68,\n \"left\": 218,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 3,\n \"lead\": true,\n \"teamScore\": 37,\n \"opponentTeamScore\": 30,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 521,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"3:44\",\n \"top\": 77,\n \"left\": 226,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 3,\n \"lead\": true,\n \"teamScore\": 44,\n \"opponentTeamScore\": 32,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 522,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"0:48\",\n \"top\": 164,\n \"left\": 217,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 11,\n \"lead\": true,\n \"teamScore\": 50,\n \"opponentTeamScore\": 35,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 523,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"11:22\",\n \"top\": 66,\n \"left\": 243,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 1,\n \"lead\": true,\n \"teamScore\": 52,\n \"opponentTeamScore\": 40,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 524,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"9:30\",\n \"top\": 149,\n \"left\": 242,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 10,\n \"lead\": true,\n \"teamScore\": 52,\n \"opponentTeamScore\": 45,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 525,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"6:26\",\n \"top\": 62,\n \"left\": 246,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 1,\n \"lead\": true,\n \"teamScore\": 59,\n \"opponentTeamScore\": 58,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 526,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"5:59\",\n \"top\": 64,\n \"left\": 232,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 1,\n \"lead\": true,\n \"teamScore\": 62,\n \"opponentTeamScore\": 60,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 527,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"5:40\",\n \"top\": 277,\n \"left\": 111,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 26,\n \"lead\": true,\n \"teamScore\": 66,\n \"opponentTeamScore\": 60,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 528,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"4:55\",\n \"top\": 78,\n \"left\": 237,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 3,\n \"lead\": true,\n \"teamScore\": 66,\n \"opponentTeamScore\": 62,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 529,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"4:24\",\n \"top\": 61,\n \"left\": 248,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 1,\n \"lead\": true,\n \"teamScore\": 70,\n \"opponentTeamScore\": 64,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 530,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"1:55\",\n \"top\": 299,\n \"left\": 313,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 26,\n \"lead\": true,\n \"teamScore\": 78,\n \"opponentTeamScore\": 68,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 531,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"11:41\",\n \"top\": 293,\n \"left\": 351,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 26,\n \"lead\": true,\n \"teamScore\": 83,\n \"opponentTeamScore\": 73,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 532,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"8:42\",\n \"top\": 213,\n \"left\": 48,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": true,\n \"teamScore\": 90,\n \"opponentTeamScore\": 79,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 533,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"7:53\",\n \"top\": 108,\n \"left\": 212,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 6,\n \"lead\": true,\n \"teamScore\": 90,\n \"opponentTeamScore\": 81,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 534,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"7:31\",\n \"top\": 350,\n \"left\": 176,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 30,\n \"lead\": true,\n \"teamScore\": 90,\n \"opponentTeamScore\": 81,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 535,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"6:33\",\n \"top\": 133,\n \"left\": 252,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 8,\n \"lead\": true,\n \"teamScore\": 90,\n \"opponentTeamScore\": 83,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 536,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"5:07\",\n \"top\": 90,\n \"left\": 363,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 13,\n \"lead\": true,\n \"teamScore\": 92,\n \"opponentTeamScore\": 88,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 537,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"4:00\",\n \"top\": 325,\n \"left\": 343,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 29,\n \"lead\": true,\n \"teamScore\": 92,\n \"opponentTeamScore\": 88,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 538,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"3:24\",\n \"top\": 58,\n \"left\": 228,\n \"playerName\": \"hardeja01\",\n \"result\": false,\n \"shotType\": \"2-pointer\",\n \"distanceFt\": 1,\n \"lead\": true,\n \"teamScore\": 92,\n \"opponentTeamScore\": 90,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 539,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"1:44\",\n \"top\": 323,\n \"left\": 323,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 28,\n \"lead\": true,\n \"teamScore\": 97,\n \"opponentTeamScore\": 94,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 540,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Dec 17,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"0:13\",\n \"top\": 287,\n \"left\": 135,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 26,\n \"lead\": true,\n \"teamScore\": 102,\n \"opponentTeamScore\": 97,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n }\n]" + } + ] + }, + { + "name": "Shot Chart 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Test to check if the response time is less than 200ms", + "pm.test(\"Response time is less than 200ms\", function () {", + " pm.expect(pm.response.responseTime).to.be.below(200);", + "});", + "", + "pm.test(\"Response status code is 200\", function () {", + " pm.expect(pm.response.code).to.eql(200);", + "});", + "", + "", + "pm.test(\"Response is an array with at least one element\", function () {", + " const responseData = pm.response.json();", + " ", + " pm.expect(responseData).to.be.an('array').that.is.not.empty;", + "});", + "", + "", + "pm.test(\"Validate the schema of the objects in the array\", function () {", + " const responseData = pm.response.json();", + "", + " pm.expect(responseData).to.be.an('array').that.is.not.empty;", + "", + " responseData.forEach(function(item) {", + " pm.expect(item).to.be.an('object');", + " pm.expect(item).to.have.all.keys('id', 'playerId', 'season', 'date', 'qtr', 'timeRemaining', 'top', 'left', 'playerName', 'result', 'shotType', 'distanceFt', 'lead', 'teamScore', 'opponentTeamScore', 'opponent', 'team', 'ID', 'CreatedAt', 'UpdatedAt', 'DeletedAt');", + " ", + " pm.expect(item.id).to.be.a('number');", + " pm.expect(item.playerId).to.be.a('string');", + " pm.expect(item.season).to.be.a('number');", + " pm.expect(item.date).to.be.a('string');", + " pm.expect(item.qtr).to.be.a('string');", + " pm.expect(item.timeRemaining).to.be.a('string');", + " pm.expect(item.top).to.be.a('number');", + " pm.expect(item.left).to.be.a('number');", + " pm.expect(item.playerName).to.be.a('string');", + " pm.expect(item.result).to.be.a('boolean');", + " pm.expect(item.shotType).to.be.a('string');", + " pm.expect(item.distanceFt).to.be.a('number');", + " pm.expect(item.lead).to.be.a('boolean');", + " pm.expect(item.teamScore).to.be.a('number');", + " pm.expect(item.opponentTeamScore).to.be.a('number');", + " pm.expect(item.opponent).to.be.a('string');", + " pm.expect(item.team).to.be.a('string');", + " pm.expect(item.ID).to.be.a('number');", + " pm.expect(item.CreatedAt).to.be.a('string');", + " pm.expect(item.UpdatedAt).to.be.a('string');", + " pm.expect(item.DeletedAt).to.satisfy(val => val === null || typeof val === 'object');", + " });", + "});", + "", + "", + "pm.test(\"Player ID is a non-empty string\", function () {", + " const responseData = pm.response.json();", + " ", + " pm.expect(responseData).to.be.an('array').that.is.not.empty;", + " responseData.forEach(function(item) {", + " pm.expect(item.playerId).to.exist.and.to.be.a('string').and.to.have.lengthOf.at.least(1, \"Player ID should not be empty\");", + " });", + "});", + "", + "", + "pm.test(\"Season is a positive integer\", function () {", + " const responseData = pm.response.json();", + " ", + " pm.expect(responseData).to.be.an('array').that.is.not.empty;", + " responseData.forEach(function(item) {", + " pm.expect(item.season).to.be.a('number').and.to.be.above(0);", + " });", + "});", + "", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{api-key}}", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{base_url}}/api/playershotchart?page=1&playerId=hardeja01&season=2019&result=true&shot_type=3-pointer", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "playershotchart" + ], + "query": [ + { + "key": "page", + "value": "1", + "description": "(Optional) The page number for pagination. Defaults to 1. Each page returns a maximum of 50 results." + }, + { + "key": "playerId", + "value": "hardeja01", + "description": "(Optional) The unique ID for the player (e.g., hardeja01)." + }, + { + "key": "season", + "value": "2019", + "description": "(Optional) The season year (e.g., 2024 for the 2023-24 season)." + }, + { + "key": "result", + "value": "true", + "description": "(Optional) Filter by shot outcome (true for made, false for missed)." + }, + { + "key": "shot_type", + "value": "3-pointer", + "description": "(Optional) The type of shot (e.g., '2-pointer', '3-pointer')." + } + ] + }, + "description": "### Get Player Shot Chart Data\n\nReturns a paginated list of shot chart data points for a player. The data can be filtered by a variety of parameters to narrow down the results.\n\n### Usage\n\nThis endpoint is ideal for visualizing player shooting performance. You can filter by a specific player, season, game date, quarter, and more.\n\n**Note:** Pagination is available via the `page` parameter, but the number of results per page is fixed at **50**.\n\n#### **Query Parameters**\n\n* **`page`** (integer, optional): The page number for pagination. Defaults to `1`.\n* **`playerId`** (string, optional): The player's unique ID (e.g., `hardeja01`).\n* **`season`** (integer, optional): The season year (e.g., `2023`).\n* **`date`** (string, optional): The specific game date (e.g., `Oct 17, 2018`).\n* **`qtr`** (string, optional): The quarter of the game (e.g., `1st Qtr`).\n* **`result`** (boolean, optional): Filter by shot outcome (`true` for made shots, `false` for missed).\n* **`shot_type`** (string, optional): The type of shot (e.g., `2-pointer`, `3-pointer`).\n* **`opponent`** (string, optional): The three-letter abbreviation for the opponent team (e.g., `NOP`)." + }, + "response": [ + { + "name": "Shot Chart 2", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{api-key}}", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{base_url}}/api/playershotchart?page=1&playerId=hardeja01&season=2019&result=true&shot_type=3-pointer", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "playershotchart" + ], + "query": [ + { + "key": "page", + "value": "1", + "description": "(Optional) The page number for pagination. Defaults to 1. Each page returns a maximum of 50 results." + }, + { + "key": "playerId", + "value": "hardeja01", + "description": "(Optional) The unique ID for the player (e.g., hardeja01)." + }, + { + "key": "season", + "value": "2019", + "description": "(Optional) The season year (e.g., 2024 for the 2023-24 season)." + }, + { + "key": "result", + "value": "true", + "description": "(Optional) Filter by shot outcome (true for made, false for missed)." + }, + { + "key": "shot_type", + "value": "3-pointer", + "description": "(Optional) The type of shot (e.g., '2-pointer', '3-pointer')." + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Alt-Svc", + "value": "h3=\":443\"; ma=2592000" + }, + { + "key": "Content-Encoding", + "value": "br" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Date", + "value": "Wed, 02 Jul 2025 01:19:06 GMT" + }, + { + "key": "Server", + "value": "nginx/1.28.0" + }, + { + "key": "Vary", + "value": "Accept-Encoding" + }, + { + "key": "Content-Length", + "value": "1291" + } + ], + "cookie": [], + "body": "[\n {\n \"id\": 4,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Oct 17,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"4:12\",\n \"top\": 284,\n \"left\": 121,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 26,\n \"lead\": false,\n \"teamScore\": 18,\n \"opponentTeamScore\": 22,\n \"opponent\": \"NOP\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 12,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Oct 17,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"6:18\",\n \"top\": 288,\n \"left\": 318,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": false,\n \"teamScore\": 95,\n \"opponentTeamScore\": 113,\n \"opponent\": \"NOP\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 15,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Oct 17,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"3:27\",\n \"top\": 284,\n \"left\": 343,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": false,\n \"teamScore\": 102,\n \"opponentTeamScore\": 121,\n \"opponent\": \"NOP\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 17,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Oct 20,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"7:48\",\n \"top\": 316,\n \"left\": 332,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 28,\n \"lead\": true,\n \"teamScore\": 14,\n \"opponentTeamScore\": 10,\n \"opponent\": \"LAL\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 23,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Oct 20,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"6:25\",\n \"top\": 293,\n \"left\": 147,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 26,\n \"lead\": true,\n \"teamScore\": 47,\n \"opponentTeamScore\": 44,\n \"opponent\": \"LAL\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 28,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Oct 20,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"0:45\",\n \"top\": 321,\n \"left\": 199,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 63,\n \"opponentTeamScore\": 62,\n \"opponent\": \"LAL\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 32,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Oct 20,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"6:24\",\n \"top\": 253,\n \"left\": 409,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 26,\n \"lead\": true,\n \"teamScore\": 109,\n \"opponentTeamScore\": 104,\n \"opponent\": \"LAL\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 33,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Oct 20,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"1:12\",\n \"top\": 270,\n \"left\": 404,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 120,\n \"opponentTeamScore\": 113,\n \"opponent\": \"LAL\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 43,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Oct 21,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"6:11\",\n \"top\": 327,\n \"left\": 170,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 28,\n \"lead\": true,\n \"teamScore\": 42,\n \"opponentTeamScore\": 39,\n \"opponent\": \"LAC\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 47,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Oct 21,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"1:44\",\n \"top\": 232,\n \"left\": 418,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": true,\n \"teamScore\": 51,\n \"opponentTeamScore\": 50,\n \"opponent\": \"LAC\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 55,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Oct 21,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"2:47\",\n \"top\": 241,\n \"left\": 407,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": false,\n \"teamScore\": 106,\n \"opponentTeamScore\": 110,\n \"opponent\": \"LAC\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 59,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Oct 21,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"0:29\",\n \"top\": 288,\n \"left\": 162,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": false,\n \"teamScore\": 111,\n \"opponentTeamScore\": 115,\n \"opponent\": \"LAC\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 62,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Oct 24,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"9:53\",\n \"top\": 271,\n \"left\": 395,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 7,\n \"opponentTeamScore\": 2,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 64,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Oct 24,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"0:51\",\n \"top\": 253,\n \"left\": 95,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": true,\n \"teamScore\": 29,\n \"opponentTeamScore\": 24,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 76,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Oct 24,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"5:50\",\n \"top\": 328,\n \"left\": 285,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 28,\n \"lead\": false,\n \"teamScore\": 52,\n \"opponentTeamScore\": 64,\n \"opponent\": \"UTA\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 84,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 3,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"3:34\",\n \"top\": 303,\n \"left\": 150,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": false,\n \"teamScore\": 47,\n \"opponentTeamScore\": 47,\n \"opponent\": \"CHI\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 85,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 3,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"2:52\",\n \"top\": 138,\n \"left\": 471,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": true,\n \"teamScore\": 50,\n \"opponentTeamScore\": 47,\n \"opponent\": \"CHI\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 89,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 3,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"5:57\",\n \"top\": 313,\n \"left\": 238,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 26,\n \"lead\": true,\n \"teamScore\": 63,\n \"opponentTeamScore\": 59,\n \"opponent\": \"CHI\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 90,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 3,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"4:47\",\n \"top\": 307,\n \"left\": 323,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 66,\n \"opponentTeamScore\": 59,\n \"opponent\": \"CHI\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 91,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 3,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"3:59\",\n \"top\": 299,\n \"left\": 182,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": true,\n \"teamScore\": 71,\n \"opponentTeamScore\": 59,\n \"opponent\": \"CHI\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 98,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 5,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"9:13\",\n \"top\": 321,\n \"left\": 262,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 11,\n \"opponentTeamScore\": 6,\n \"opponent\": \"IND\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 99,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 5,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"7:27\",\n \"top\": 282,\n \"left\": 104,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 16,\n \"opponentTeamScore\": 10,\n \"opponent\": \"IND\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 101,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 5,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"4:06\",\n \"top\": 78,\n \"left\": 477,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 24,\n \"lead\": true,\n \"teamScore\": 24,\n \"opponentTeamScore\": 18,\n \"opponent\": \"IND\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 106,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 5,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"0:09\",\n \"top\": 328,\n \"left\": 309,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 28,\n \"lead\": false,\n \"teamScore\": 53,\n \"opponentTeamScore\": 55,\n \"opponent\": \"IND\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 111,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 5,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"0:36\",\n \"top\": 267,\n \"left\": 77,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 93,\n \"opponentTeamScore\": 90,\n \"opponent\": \"IND\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 114,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 8,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"6:32\",\n \"top\": 252,\n \"left\": 390,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": false,\n \"teamScore\": 12,\n \"opponentTeamScore\": 15,\n \"opponent\": \"OKC\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 120,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 8,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"3:14\",\n \"top\": 292,\n \"left\": 337,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 26,\n \"lead\": false,\n \"teamScore\": 43,\n \"opponentTeamScore\": 47,\n \"opponent\": \"OKC\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 123,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 8,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"7:03\",\n \"top\": 202,\n \"left\": 445,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": false,\n \"teamScore\": 55,\n \"opponentTeamScore\": 66,\n \"opponent\": \"OKC\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 125,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 8,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"8:24\",\n \"top\": 287,\n \"left\": 331,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": false,\n \"teamScore\": 67,\n \"opponentTeamScore\": 86,\n \"opponent\": \"OKC\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 140,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 10,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"1:35\",\n \"top\": 313,\n \"left\": 234,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 26,\n \"lead\": true,\n \"teamScore\": 48,\n \"opponentTeamScore\": 47,\n \"opponent\": \"SAS\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 160,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 11,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"5:13\",\n \"top\": 277,\n \"left\": 88,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 20,\n \"opponentTeamScore\": 16,\n \"opponent\": \"IND\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 163,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 11,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"6:26\",\n \"top\": 305,\n \"left\": 151,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 49,\n \"opponentTeamScore\": 39,\n \"opponent\": \"IND\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 164,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 11,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"5:00\",\n \"top\": 289,\n \"left\": 120,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 55,\n \"opponentTeamScore\": 44,\n \"opponent\": \"IND\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 165,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 11,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"2:29\",\n \"top\": 297,\n \"left\": 348,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 67,\n \"opponentTeamScore\": 50,\n \"opponent\": \"IND\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 166,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 11,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"2:07\",\n \"top\": 344,\n \"left\": 238,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 29,\n \"lead\": true,\n \"teamScore\": 70,\n \"opponentTeamScore\": 50,\n \"opponent\": \"IND\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 170,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 11,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"4:27\",\n \"top\": 289,\n \"left\": 105,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 87,\n \"opponentTeamScore\": 74,\n \"opponent\": \"IND\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 172,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 11,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"3:35\",\n \"top\": 310,\n \"left\": 132,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 28,\n \"lead\": true,\n \"teamScore\": 91,\n \"opponentTeamScore\": 75,\n \"opponent\": \"IND\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 174,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 11,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"7:04\",\n \"top\": 313,\n \"left\": 298,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 107,\n \"opponentTeamScore\": 92,\n \"opponent\": \"IND\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 186,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 13,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"3:40\",\n \"top\": 277,\n \"left\": 348,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": true,\n \"teamScore\": 79,\n \"opponentTeamScore\": 74,\n \"opponent\": \"DEN\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 190,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 13,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"4:50\",\n \"top\": 248,\n \"left\": 397,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": true,\n \"teamScore\": 102,\n \"opponentTeamScore\": 91,\n \"opponent\": \"DEN\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 199,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 15,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"2:50\",\n \"top\": 264,\n \"left\": 376,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": true,\n \"teamScore\": 16,\n \"opponentTeamScore\": 15,\n \"opponent\": \"GSW\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 208,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 15,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"0:29\",\n \"top\": 330,\n \"left\": 204,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 28,\n \"lead\": true,\n \"teamScore\": 47,\n \"opponentTeamScore\": 39,\n \"opponent\": \"GSW\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 210,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 15,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"11:37\",\n \"top\": 274,\n \"left\": 64,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 28,\n \"lead\": true,\n \"teamScore\": 50,\n \"opponentTeamScore\": 41,\n \"opponent\": \"GSW\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 213,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 15,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"5:30\",\n \"top\": 231,\n \"left\": 38,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 63,\n \"opponentTeamScore\": 47,\n \"opponent\": \"GSW\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 227,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 17,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"7:44\",\n \"top\": 313,\n \"left\": 213,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 26,\n \"lead\": true,\n \"teamScore\": 83,\n \"opponentTeamScore\": 69,\n \"opponent\": \"SAC\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 229,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 17,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"4:24\",\n \"top\": 313,\n \"left\": 299,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 95,\n \"opponentTeamScore\": 74,\n \"opponent\": \"SAC\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 230,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 17,2018\",\n \"qtr\": \"3rd Qtr\",\n \"timeRemaining\": \"4:06\",\n \"top\": 295,\n \"left\": 369,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 27,\n \"lead\": true,\n \"teamScore\": 98,\n \"opponentTeamScore\": 74,\n \"opponent\": \"SAC\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 233,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 17,2018\",\n \"qtr\": \"4th Qtr\",\n \"timeRemaining\": \"3:46\",\n \"top\": 154,\n \"left\": 7,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": true,\n \"teamScore\": 126,\n \"opponentTeamScore\": 103,\n \"opponent\": \"SAC\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 238,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 21,2018\",\n \"qtr\": \"1st Qtr\",\n \"timeRemaining\": \"4:40\",\n \"top\": 261,\n \"left\": 384,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 25,\n \"lead\": true,\n \"teamScore\": 15,\n \"opponentTeamScore\": 14,\n \"opponent\": \"DET\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n },\n {\n \"id\": 241,\n \"playerId\": \"hardeja01\",\n \"season\": 2019,\n \"date\": \"Nov 21,2018\",\n \"qtr\": \"2nd Qtr\",\n \"timeRemaining\": \"6:38\",\n \"top\": 243,\n \"left\": 415,\n \"playerName\": \"hardeja01\",\n \"result\": true,\n \"shotType\": \"3-pointer\",\n \"distanceFt\": 26,\n \"lead\": true,\n \"teamScore\": 45,\n \"opponentTeamScore\": 44,\n \"opponent\": \"DET\",\n \"team\": \"HOU\",\n \"ID\": 0,\n \"CreatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"UpdatedAt\": \"2025-06-20T22:48:34.764255Z\",\n \"DeletedAt\": null\n }\n]" + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "api-key", + "value": "", + "type": "string", + "disabled": true + }, + { + "key": "x-secret", + "value": "", + "type": "string", + "disabled": true + }, + { + "key": "shot_player", + "value": "jokicni01" + }, + { + "key": "base_url", + "value": "https://api.server.nbaapi.com", + "type": "string" + }, + { + "key": "base_url_2", + "value": "https://nbago.server.nbaapi.com", + "type": "string" + } + ] +} \ No newline at end of file From fbae36e06e87b4cf5ae54e4430f6a2dea8ecb891 Mon Sep 17 00:00:00 2001 From: Ravi Prasad Date: Thu, 3 Jul 2025 21:09:09 -0500 Subject: [PATCH 02/10] dev mode returned local --- Dockerfile | 3 +- docker-compose.local.yml | 63 +++++++++++++++++++++++++++------------- docs/docs.go | 2 +- docs/swagger.json | 2 +- docs/swagger.yaml | 2 +- import.go | 8 ++--- main.go | 5 ++-- 7 files changed, 55 insertions(+), 30 deletions(-) diff --git a/Dockerfile b/Dockerfile index d8d3a18..0700431 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,8 @@ FROM golang:1.24-bullseye AS builder ENV CGO_ENABLED=1 ENV GOOS=linux # Changed from Apple Silicon arm64 config. -ENV GOARCH=amd64 +# ENV GOARCH=amd64 +ENV GOARCH=arm64 WORKDIR /app diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 5c32137..b88f1ee 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -1,49 +1,72 @@ -# docker-compose.local.yml -# -# Use this file to run local services (API, Nginx, etc.) while -# connecting to a REMOTE database. -# -# This version includes the 'db-init' service. Note that this service -# will attempt to run its 'import-data' command every time you execute -# 'docker-compose up'. +version: '3.8' services: - # This service runs the 'import-data' command against the remote database - # specified in your '.env.local' file. It has no 'depends_on' since there - # is no local postgres container to wait for. + # NEW: Add a dedicated PostgreSQL database service + postgres: + image: postgres:15-alpine + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + ports: + - '${DB_PORT}:5432' # Expose DB port to host machine for local tools + volumes: + - postgres_data:/var/lib/postgresql/data # Persist data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - api.network + + # MODIFIED: This service now runs migrations and imports against the live Postgres DB + # "import-data" on initial run to migrate and import data. db-init: build: . - env_file: .env.local - command: ["/nba_go", "import-data"] + env_file: .env + command: ["/nba_go"] + depends_on: + postgres: + condition: service_healthy networks: - api.network restart: "no" - # API services now have no 'depends_on' section. - # They will start and immediately try to connect to the DB_HOST in your .env file. + # MODIFIED: All API services now depend on postgres being healthy. + # The volume mount for './data' is no longer needed. api1: build: . - env_file: .env.local + env_file: .env ports: - '5001:5000' + depends_on: + postgres: + condition: service_healthy networks: - api.network restart: always api2: build: . - env_file: .env.local + env_file: .env ports: - '5002:5000' + depends_on: + postgres: + condition: service_healthy networks: - api.network restart: always api3: build: . - env_file: .env.local + env_file: .env ports: - '5003:5000' + depends_on: + postgres: + condition: service_healthy networks: - api.network restart: always @@ -57,7 +80,7 @@ services: - api2 - api3 ports: - - '8080:8080' + - '8081:8080' networks: - api.network @@ -93,7 +116,7 @@ services: restart: unless-stopped volumes: - # The 'postgres_data' volume is not needed in this configuration. + postgres_data: # New volume for Postgres prometheus_data: grafana_data: diff --git a/docs/docs.go b/docs/docs.go index 2992c7b..22f31b5 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -477,7 +477,7 @@ var SwaggerInfo = &swag.Spec{ Version: "1.0", Host: "", BasePath: "/", - Schemes: []string{"https"}, + Schemes: []string{"http"}, Title: "NBA_Go API", Description: "Stats service, now with public access!", InfoInstanceName: "swagger", diff --git a/docs/swagger.json b/docs/swagger.json index 6ee7f8c..7555ca2 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1,6 +1,6 @@ { "schemes": [ - "https" + "http" ], "swagger": "2.0", "info": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 8988b31..21705c3 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -303,7 +303,7 @@ paths: - PlayerTotals x-order: 1 schemes: -- https +- http swagger: "2.0" tags: - name: PlayerTotals diff --git a/import.go b/import.go index d994189..91ca099 100644 --- a/import.go +++ b/import.go @@ -11,7 +11,7 @@ import ( // importPlayerAdvanced fetches and stores advanced stats for seasons 2017–2025 func importPlayerAdvanced(db *gorm.DB) { - for season := 1991; season <= 2002; season++ { + for season := 2024; season <= 2025; season++ { if err := services.FetchAndStorePlayerAdvancedScrapedStats(db, season, false); err != nil { log.Printf("advanced import failed for %d: %v", season, err) } @@ -23,7 +23,7 @@ func importPlayerAdvanced(db *gorm.DB) { // importPlayerAdvancedPlayoffs fetches and stores advanced stats for playoffs seasons 2023–2025 func importPlayerAdvancedPlayoffs(db *gorm.DB) { - for season := 1991; season <= 2002; season++ { + for season := 2024; season <= 2025; season++ { if err := services.FetchAndStorePlayerAdvancedScrapedStats(db, season, true); err != nil { log.Printf("advanced import failed for %d: %v", season, err) } @@ -35,7 +35,7 @@ func importPlayerAdvancedPlayoffs(db *gorm.DB) { // importPlayerTotalsScrape fetches & stores scraped regular-season total stats func importPlayerTotalsScrape(db *gorm.DB) { - for season := 1991; season <= 2002; season++ { + for season := 2024; season <= 2025; season++ { if err := services.FetchAndStorePlayerTotalScrapedStats(db, season, false); err != nil { log.Printf("scraped totals import failed for %d: %v", season, err) } @@ -47,7 +47,7 @@ func importPlayerTotalsScrape(db *gorm.DB) { // importPlayerPlayoffsScrape fetches & stores scraped playoff total stats func importPlayerTotalsPlayoffsScrape(db *gorm.DB) { - for season := 1991; season <= 2002; season++ { + for season := 2024; season <= 2025; season++ { if err := services.FetchAndStorePlayerTotalScrapedStats(db, season, true); err != nil { log.Printf("scraped playoffs import failed for %d: %v", season, err) } diff --git a/main.go b/main.go index ef24fab..49672bf 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,8 @@ // @title NBA_Go API // @version 1.0 // @description Stats service, now with public access! -// @schemes https +// //@schemes https +// @schemes http // @BasePath / // // @tag.name PlayerTotals @@ -45,7 +46,7 @@ import ( func main() { // ——— One-off import-data mode ——— if len(os.Args) > 1 && os.Args[1] == "import-data" { - // Run all migrations + import steps exactly once + // Run all DB migrations + import steps exactly once db := config.InitDB(true) importPlayerAdvanced(db) From 30d47bbafa1ebf12c5078473b57744bcb397a2ef Mon Sep 17 00:00:00 2001 From: Ravi Prasad Date: Thu, 3 Jul 2025 21:13:46 -0500 Subject: [PATCH 03/10] remove monitoring logs docker --- README.md | 7 +++++++ docker-compose.local.yml | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/README.md b/README.md index 5d05985..30899d3 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,10 @@ swag init -g main.go -o docs ```bash go run loadtest.go -n 100 -c 10 -url "http://127.0.0.1:8080/api/playeradvancedstats?page=1&pageSize=20" -log results.log -key "xxx" ``` + + +### Local Environment + +```bash +docker-compose -f docker-compose.local.yml up --build -d +``` \ No newline at end of file diff --git a/docker-compose.local.yml b/docker-compose.local.yml index b88f1ee..0968822 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -99,6 +99,8 @@ services: networks: - api.network restart: unless-stopped + logging: + driver: "none" grafana: image: grafana/grafana:latest @@ -114,6 +116,8 @@ services: networks: - api.network restart: unless-stopped + logging: + driver: none volumes: postgres_data: # New volume for Postgres From cbbb1aef4465d97ad9e66129c91cb2d0e8a9e87c Mon Sep 17 00:00:00 2001 From: Ravi Prasad Date: Fri, 4 Jul 2025 02:02:15 -0500 Subject: [PATCH 04/10] new game model --- models/game.go | 174 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 models/game.go diff --git a/models/game.go b/models/game.go new file mode 100644 index 0000000..b467faf --- /dev/null +++ b/models/game.go @@ -0,0 +1,174 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// Game represents the top-level information for a single NBA game, including schedule details and results. +type Game struct { + ID uint `gorm:"primaryKey" swaggerignore:"true"` + GameID string `gorm:"not null;uniqueIndex" json:"gameId"` // A unique identifier for the game, e.g., "202504010ATL" + Date time.Time `gorm:"not null;index" json:"date"` + IsPlayoff bool `gorm:"not null;default:false;index" json:"isPlayoff"` + StartTimeET string `json:"startTimeET"` + Arena string `json:"arena"` + VisitorTeam string `gorm:"not null" json:"visitorTeam"` + VisitorPTS int `json:"visitorPts"` + HomeTeam string `gorm:"not null" json:"homeTeam"` + HomePTS int `json:"homePts"` + GameDuration string `json:"gameDuration"` // e.g., "2:23" + BoxScoreURL string `json:"boxScoreUrl"` + + // Associations + LineScores []LineScore `json:"lineScores"` + PlayerGameBasicStats []PlayerGameBasicStat `json:"playerGameBasicStats"` + PlayerGameAdvStats []PlayerGameAdvStat `json:"playerGameAdvStats"` + TeamGameBasicStats []TeamGameBasicStat `json:"teamGameBasicStats"` + TeamGameAdvStats []TeamGameAdvStat `json:"teamGameAdvStats"` + + CreatedAt time.Time `swaggerignore:"true"` + UpdatedAt time.Time `swaggerignore:"true"` + DeletedAt gorm.DeletedAt `gorm:"index" swaggerignore:"true"` +} + +// LineScore holds the scoring for each team by quarter for a specific game. +type LineScore struct { + ID uint `gorm:"primaryKey" swaggerignore:"true"` + GameID string `gorm:"not null;index" json:"gameId"` + Team string `gorm:"not null;index" json:"team"` + Q1 int `json:"q1"` + Q2 int `json:"q2"` + Q3 int `json:"q3"` + Q4 int `json:"q4"` + // Overtime scores; will be 0 if the game did not go to the respective OT period. + OT1 int `json:"ot1"` + OT2 int `json:"ot2"` + OT3 int `json:"ot3"` + Total int `json:"total"` + + CreatedAt time.Time `swaggerignore:"true"` + UpdatedAt time.Time `swaggerignore:"true"` + DeletedAt gorm.DeletedAt `gorm:"index" swaggerignore:"true"` +} + +// PlayerGameBasicStat contains the basic box score statistics for a single player in a single game. +type PlayerGameBasicStat struct { + ID uint `gorm:"primaryKey" swaggerignore:"true"` + GameID string `gorm:"not null;uniqueIndex:idx_player_game_basic" json:"gameId"` + PlayerID string `gorm:"not null;uniqueIndex:idx_player_game_basic" json:"playerId"` + PlayerName string `json:"playerName"` + Team string `gorm:"not null" json:"team"` + Status string `json:"status"` // e.g., "Starter", "Bench", "Did Not Play" + MP string `json:"mp"` // Minutes Played, e.g., "38:07" + FG int `json:"fg"` + FGA int `json:"fga"` + FGPercent float64 `json:"fgPercent"` + ThreeP int `json:"threeP"` + ThreePA int `json:"threePa"` + ThreePPercent float64 `json:"threePPercent"` + FT int `json:"ft"` + FTA int `json:"fta"` + FTPercent float64 `json:"ftPercent"` + ORB int `json:"orb"` + DRB int `json:"drb"` + TRB int `json:"trb"` + AST int `json:"ast"` + STL int `json:"stl"` + BLK int `json:"blk"` + TOV int `json:"tov"` + PF int `json:"pf"` + PTS int `json:"pts"` + GmSc float64 `json:"gmSc"` // Game Score + PlusMinus int `json:"plusMinus"` + + CreatedAt time.Time `swaggerignore:"true"` + UpdatedAt time.Time `swaggerignore:"true"` + DeletedAt gorm.DeletedAt `gorm:"index" swaggerignore:"true"` +} + +// PlayerGameAdvStat contains the advanced box score statistics for a single player in a single game. +type PlayerGameAdvStat struct { + ID uint `gorm:"primaryKey" swaggerignore:"true"` + GameID string `gorm:"not null;uniqueIndex:idx_player_game_adv" json:"gameId"` + PlayerID string `gorm:"not null;uniqueIndex:idx_player_game_adv" json:"playerId"` + PlayerName string `json:"playerName"` + Team string `gorm:"not null" json:"team"` + MP string `json:"mp"` // Minutes Played + TSPercent float64 `json:"tsPercent"` + EFGPercent float64 `json:"efgPercent"` + ThreePAr float64 `json:"threePAr"` + FTr float64 `json:"fTr"` + ORBPercent float64 `json:"orbPercent"` + DRBPercent float64 `json:"drbPercent"` + TRBPercent float64 `json:"trbPercent"` + ASTPercent float64 `json:"astPercent"` + STLPercent float64 `json:"stlPercent"` + BLKPercent float64 `json:"blkPercent"` + TOVPercent float64 `json:"tovPercent"` + USGPercent float64 `json:"usgPercent"` + ORtg int `json:"oRtg"` + DRtg int `json:"dRtg"` + BPM float64 `json:"bpm"` // Box Plus/Minus + + CreatedAt time.Time `swaggerignore:"true"` + UpdatedAt time.Time `swaggerignore:"true"` + DeletedAt gorm.DeletedAt `gorm:"index" swaggerignore:"true"` +} + +// TeamGameBasicStat holds the total basic stats for a team in a single game. +type TeamGameBasicStat struct { + ID uint `gorm:"primaryKey" swaggerignore:"true"` + GameID string `gorm:"not null;uniqueIndex:idx_team_game_basic" json:"gameId"` + Team string `gorm:"not null;uniqueIndex:idx_team_game_basic" json:"team"` + MP int `json:"mp"` // Total minutes, usually 240 for a regulation game + FG int `json:"fg"` + FGA int `json:"fga"` + FGPercent float64 `json:"fgPercent"` + ThreeP int `json:"threeP"` + ThreePA int `json:"threePa"` + ThreePPercent float64 `json:"threePPercent"` + FT int `json:"ft"` + FTA int `json:"fta"` + FTPercent float64 `json:"ftPercent"` + ORB int `json:"orb"` + DRB int `json:"drb"` + TRB int `json:"trb"` + AST int `json:"ast"` + STL int `json:"stl"` + BLK int `json:"blk"` + TOV int `json:"tov"` + PF int `json:"pf"` + PTS int `json:"pts"` + + CreatedAt time.Time `swaggerignore:"true"` + UpdatedAt time.Time `swaggerignore:"true"` + DeletedAt gorm.DeletedAt `gorm:"index" swaggerignore:"true"` +} + +// TeamGameAdvStat holds the total advanced stats for a team in a single game. +type TeamGameAdvStat struct { + ID uint `gorm:"primaryKey" swaggerignore:"true"` + GameID string `gorm:"not null;uniqueIndex:idx_team_game_adv" json:"gameId"` + Team string `gorm:"not null;uniqueIndex:idx_team_game_adv" json:"team"` + MP int `json:"mp"` + TSPercent float64 `json:"tsPercent"` + EFGPercent float64 `json:"efgPercent"` + ThreePAr float64 `json:"threePAr"` + FTr float64 `json:"fTr"` + ORBPercent float64 `json:"orbPercent"` + DRBPercent float64 `json:"drbPercent"` + TRBPercent float64 `json:"trbPercent"` + ASTPercent float64 `json:"astPercent"` + STLPercent float64 `json:"stlPercent"` + BLKPercent float64 `json:"blkPercent"` + TOVPercent float64 `json:"tovPercent"` + USGPercent float64 `json:"usgPercent"` // Will be ~100.0 for a team + ORtg float64 `json:"oRtg"` + DRtg float64 `json:"dRtg"` + + CreatedAt time.Time `swaggerignore:"true"` + UpdatedAt time.Time `swaggerignore:"true"` + DeletedAt gorm.DeletedAt `gorm:"index" swaggerignore:"true"` +} From a16af088afd42e999e801f7ad2c05073ed48e0bd Mon Sep 17 00:00:00 2001 From: Ravi Prasad Date: Fri, 4 Jul 2025 03:12:59 -0500 Subject: [PATCH 05/10] models migrated and data insert success with dbeaver --- .DS_Store | Bin 30724 -> 30724 bytes config/database.go | 13 ++++++++++++- docker-compose.local.yml | 2 +- import.go | 8 ++++---- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.DS_Store b/.DS_Store index 6fc23a9eb36db554e39108b6ae946cc38bdf55b1..bd3a02a9b143d9e07297e1bceea6afa0adca19e7 100644 GIT binary patch delta 71 zcmZqqz}WJEaYLRli# delta 29 lcmZqqz}WJEaYLT*WNvGr$(4HRHXE4y Date: Tue, 8 Jul 2025 01:23:34 -0500 Subject: [PATCH 06/10] per game stats and box score --- config/database.go | 29 ++- import.go | 51 +++- main.go | 7 + models/game.go | 184 ++++++++------- services/box_score_scrape_service.go | 334 +++++++++++++++++++++++++++ services/game_scrape_service.go | 190 +++++++++++++++ services/helpers.go | 10 + 7 files changed, 704 insertions(+), 101 deletions(-) create mode 100644 services/box_score_scrape_service.go create mode 100644 services/game_scrape_service.go diff --git a/config/database.go b/config/database.go index 2f779b3..080bf78 100644 --- a/config/database.go +++ b/config/database.go @@ -1,3 +1,5 @@ +// File: config/database.go + package config import ( @@ -12,7 +14,7 @@ import ( ) func InitDB(shouldMigrate bool) *gorm.DB { - // CHANGE: Build the DSN from environment variables + // ... (DSN setup code is unchanged) ... dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=UTC", os.Getenv("DB_HOST"), os.Getenv("DB_USER"), @@ -21,13 +23,32 @@ func InitDB(shouldMigrate bool) *gorm.DB { os.Getenv("DB_PORT"), ) - db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) // <- CHANGE: Use the postgres driver + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { log.Fatalf("failed to connect database: %v", err) } metrics.DBOperationsTotal.WithLabelValues("connect", "database").Inc() if shouldMigrate { + // --- ADD THIS BLOCK TO DROP STALE TABLES --- + // Drop tables in reverse order of dependency (children first). + // This ensures a clean migration every time the import process runs. + log.Println("⚠️ Dropping existing game-related tables for a clean migration...") + if err := db.Migrator().DropTable( + &models.LineScore{}, + &models.PlayerGameBasicStat{}, + &models.PlayerGameAdvStat{}, + &models.TeamGameBasicStat{}, + &models.TeamGameAdvStat{}, + &models.Game{}, // Drop parent table last + ); err != nil { + log.Fatalf("failed to drop tables: %v", err) + } + log.Println("✅ Tables dropped successfully.") + // --- END OF ADDED BLOCK --- + + + // Your existing AutoMigrate calls will now work correctly if err := db.AutoMigrate(&models.PlayerAdvancedStat{}); err != nil { log.Fatalf("migrate PlayerAdvancedStat: %v", err) } @@ -40,7 +61,7 @@ func InitDB(shouldMigrate bool) *gorm.DB { if err := db.AutoMigrate(&models.APIKey{}); err != nil { log.Fatalf("migrate APIKey: %v", err) } - // Game Models + // Game Models will be recreated with the correct schema if err := db.AutoMigrate( &models.Game{}, &models.LineScore{}, @@ -55,4 +76,4 @@ func InitDB(shouldMigrate bool) *gorm.DB { } return db -} +} \ No newline at end of file diff --git a/import.go b/import.go index 17043aa..4792e7d 100644 --- a/import.go +++ b/import.go @@ -11,7 +11,7 @@ import ( // importPlayerAdvanced fetches and stores advanced stats for seasons 2017–2025 func importPlayerAdvanced(db *gorm.DB) { - for season := 2022; season <= 2025; season++ { + for season := 2025; season <= 2025; season++ { if err := services.FetchAndStorePlayerAdvancedScrapedStats(db, season, false); err != nil { log.Printf("advanced import failed for %d: %v", season, err) } @@ -23,7 +23,7 @@ func importPlayerAdvanced(db *gorm.DB) { // importPlayerAdvancedPlayoffs fetches and stores advanced stats for playoffs seasons 2023–2025 func importPlayerAdvancedPlayoffs(db *gorm.DB) { - for season := 2022; season <= 2025; season++ { + for season := 2025; season <= 2025; season++ { if err := services.FetchAndStorePlayerAdvancedScrapedStats(db, season, true); err != nil { log.Printf("advanced import failed for %d: %v", season, err) } @@ -35,7 +35,7 @@ func importPlayerAdvancedPlayoffs(db *gorm.DB) { // importPlayerTotalsScrape fetches & stores scraped regular-season total stats func importPlayerTotalsScrape(db *gorm.DB) { - for season := 2022; season <= 2025; season++ { + for season := 2025; season <= 2025; season++ { if err := services.FetchAndStorePlayerTotalScrapedStats(db, season, false); err != nil { log.Printf("scraped totals import failed for %d: %v", season, err) } @@ -47,7 +47,7 @@ func importPlayerTotalsScrape(db *gorm.DB) { // importPlayerPlayoffsScrape fetches & stores scraped playoff total stats func importPlayerTotalsPlayoffsScrape(db *gorm.DB) { - for season := 2022; season <= 2025; season++ { + for season := 2025; season <= 2025; season++ { if err := services.FetchAndStorePlayerTotalScrapedStats(db, season, true); err != nil { log.Printf("scraped playoffs import failed for %d: %v", season, err) } @@ -57,6 +57,49 @@ func importPlayerTotalsPlayoffsScrape(db *gorm.DB) { } } +// importGameSchedules fetches and stores game schedules +func importGameSchedules(db *gorm.DB) { + // An NBA season typically runs from October to June + months := []string{ + "october", "november", "december", "january", + "february", "march", "april", "may", "june", + } + + for season := 2025; season <= 2025; season++ { + log.Printf("--- Starting Game Schedule Import for Season: %d ---", season) + for _, month := range months { + // The service will print a warning and skip if a month has no data (e.g. May/June for a season not yet finished) + if err := services.FetchAndStoreGameSchedule(db, season, month); err != nil { + // Log the error but continue to the next month/season + log.Printf("Game schedule import failed for %s %d: %v", month, season, err) + } + log.Printf("Game schedule import for %s, %d complete.", month, season) + // Respectful delay between requests + time.Sleep(1100 * time.Millisecond) + utils.SleepWithJitter(1000 * time.Millisecond) + } + log.Printf("--- Finished Game Schedule Import for Season: %d ---", season) + } +} + + +// importBoxScores fetches and stores all box score data (line scores, player/team stats) +// for games within a recent date range. +func importBoxScores(db *gorm.DB) { + // Define the date range for the import. + // This example fetches data for games in the last 90 days. + to := time.Now() + from := to.AddDate(0, -1, 1) // 0 years, -3 months, 0 days + + log.Printf("--- Starting Box Score Data Import from %s to %s ---", from.Format("2006-01-02"), to.Format("2006-01-02")) + + if err := services.FetchAndStoreBoxScoreDataForDateRange(db, from, to); err != nil { + log.Fatalf("Box score import failed: %v", err) + } + + log.Printf("--- Finished Box Score Data Import ---") +} + diff --git a/main.go b/main.go index 49672bf..48eadbb 100644 --- a/main.go +++ b/main.go @@ -61,6 +61,13 @@ func main() { importPlayerTotalsPlayoffsScrape(db) log.Println("🎉 Player Playoffs (scraped) Import completed successfully") + importGameSchedules(db) + log.Println("🎉 Game Imports completed successfully 🏀") + + importBoxScores(db) + log.Println("🎉 Related Box-Score Imports completed successfully 📦") + + log.Println("🏀 ALL Imports completed successfully ✅ 🙌") return } diff --git a/models/game.go b/models/game.go index b467faf..6ca8ff7 100644 --- a/models/game.go +++ b/models/game.go @@ -1,32 +1,30 @@ package models import ( - "time" - "gorm.io/gorm" + "time" ) -// Game represents the top-level information for a single NBA game, including schedule details and results. type Game struct { - ID uint `gorm:"primaryKey" swaggerignore:"true"` - GameID string `gorm:"not null;uniqueIndex" json:"gameId"` // A unique identifier for the game, e.g., "202504010ATL" - Date time.Time `gorm:"not null;index" json:"date"` - IsPlayoff bool `gorm:"not null;default:false;index" json:"isPlayoff"` - StartTimeET string `json:"startTimeET"` - Arena string `json:"arena"` - VisitorTeam string `gorm:"not null" json:"visitorTeam"` - VisitorPTS int `json:"visitorPts"` - HomeTeam string `gorm:"not null" json:"homeTeam"` - HomePTS int `json:"homePts"` - GameDuration string `json:"gameDuration"` // e.g., "2:23" - BoxScoreURL string `json:"boxScoreUrl"` + ID uint `gorm:"primaryKey" swaggerignore:"true"` + GameID string `gorm:"not null;uniqueIndex" json:"gameId"` // The field you want to use + Date time.Time `gorm:"not null;index" json:"date"` + IsPlayoff bool `gorm:"not null;default:false;index" json:"isPlayoff"` + StartTimeET string `json:"startTimeET"` + Arena string `json:"arena"` + VisitorTeam string `gorm:"not null" json:"visitorTeam"` + VisitorPTS int `json:"visitorPts"` + HomeTeam string `gorm:"not null" json:"homeTeam"` + HomePTS int `json:"homePts"` + GameDuration string `json:"gameDuration"` + BoxScoreURL string `json:"boxScoreUrl"` - // Associations - LineScores []LineScore `json:"lineScores"` - PlayerGameBasicStats []PlayerGameBasicStat `json:"playerGameBasicStats"` - PlayerGameAdvStats []PlayerGameAdvStat `json:"playerGameAdvStats"` - TeamGameBasicStats []TeamGameBasicStat `json:"teamGameBasicStats"` - TeamGameAdvStats []TeamGameAdvStat `json:"teamGameAdvStats"` + // Associations (Updated with explicit foreign key tags) + LineScores []LineScore `gorm:"foreignKey:GameID;references:GameID" json:"lineScores"` + PlayerGameBasicStats []PlayerGameBasicStat `gorm:"foreignKey:GameID;references:GameID" json:"playerGameBasicStats"` + PlayerGameAdvStats []PlayerGameAdvStat `gorm:"foreignKey:GameID;references:GameID" json:"playerGameAdvStats"` + TeamGameBasicStats []TeamGameBasicStat `gorm:"foreignKey:GameID;references:GameID" json:"teamGameBasicStats"` + TeamGameAdvStats []TeamGameAdvStat `gorm:"foreignKey:GameID;references:GameID" json:"teamGameAdvStats"` CreatedAt time.Time `swaggerignore:"true"` UpdatedAt time.Time `swaggerignore:"true"` @@ -43,10 +41,10 @@ type LineScore struct { Q3 int `json:"q3"` Q4 int `json:"q4"` // Overtime scores; will be 0 if the game did not go to the respective OT period. - OT1 int `json:"ot1"` - OT2 int `json:"ot2"` - OT3 int `json:"ot3"` - Total int `json:"total"` + OT1 int `json:"ot1"` + OT2 int `json:"ot2"` + OT3 int `json:"ot3"` + Total int `json:"total"` CreatedAt time.Time `swaggerignore:"true"` UpdatedAt time.Time `swaggerignore:"true"` @@ -55,33 +53,33 @@ type LineScore struct { // PlayerGameBasicStat contains the basic box score statistics for a single player in a single game. type PlayerGameBasicStat struct { - ID uint `gorm:"primaryKey" swaggerignore:"true"` - GameID string `gorm:"not null;uniqueIndex:idx_player_game_basic" json:"gameId"` - PlayerID string `gorm:"not null;uniqueIndex:idx_player_game_basic" json:"playerId"` - PlayerName string `json:"playerName"` - Team string `gorm:"not null" json:"team"` - Status string `json:"status"` // e.g., "Starter", "Bench", "Did Not Play" - MP string `json:"mp"` // Minutes Played, e.g., "38:07" - FG int `json:"fg"` - FGA int `json:"fga"` - FGPercent float64 `json:"fgPercent"` - ThreeP int `json:"threeP"` - ThreePA int `json:"threePa"` + ID uint `gorm:"primaryKey" swaggerignore:"true"` + GameID string `gorm:"not null;uniqueIndex:idx_player_game_basic" json:"gameId"` + PlayerID string `gorm:"not null;uniqueIndex:idx_player_game_basic" json:"playerId"` + PlayerName string `json:"playerName"` + Team string `gorm:"not null" json:"team"` + Status string `json:"status"` // e.g., "Starter", "Bench", "Did Not Play" + MP string `json:"mp"` // Minutes Played, e.g., "38:07" + FG int `json:"fg"` + FGA int `json:"fga"` + FGPercent float64 `json:"fgPercent"` + ThreeP int `json:"threeP"` + ThreePA int `json:"threePa"` ThreePPercent float64 `json:"threePPercent"` - FT int `json:"ft"` - FTA int `json:"fta"` - FTPercent float64 `json:"ftPercent"` - ORB int `json:"orb"` - DRB int `json:"drb"` - TRB int `json:"trb"` - AST int `json:"ast"` - STL int `json:"stl"` - BLK int `json:"blk"` - TOV int `json:"tov"` - PF int `json:"pf"` - PTS int `json:"pts"` - GmSc float64 `json:"gmSc"` // Game Score - PlusMinus int `json:"plusMinus"` + FT int `json:"ft"` + FTA int `json:"fta"` + FTPercent float64 `json:"ftPercent"` + ORB int `json:"orb"` + DRB int `json:"drb"` + TRB int `json:"trb"` + AST int `json:"ast"` + STL int `json:"stl"` + BLK int `json:"blk"` + TOV int `json:"tov"` + PF int `json:"pf"` + PTS int `json:"pts"` + GmSc float64 `json:"gmSc"` // Game Score + PlusMinus int `json:"plusMinus"` CreatedAt time.Time `swaggerignore:"true"` UpdatedAt time.Time `swaggerignore:"true"` @@ -90,16 +88,16 @@ type PlayerGameBasicStat struct { // PlayerGameAdvStat contains the advanced box score statistics for a single player in a single game. type PlayerGameAdvStat struct { - ID uint `gorm:"primaryKey" swaggerignore:"true"` - GameID string `gorm:"not null;uniqueIndex:idx_player_game_adv" json:"gameId"` - PlayerID string `gorm:"not null;uniqueIndex:idx_player_game_adv" json:"playerId"` - PlayerName string `json:"playerName"` - Team string `gorm:"not null" json:"team"` - MP string `json:"mp"` // Minutes Played - TSPercent float64 `json:"tsPercent"` + ID uint `gorm:"primaryKey" swaggerignore:"true"` + GameID string `gorm:"not null;uniqueIndex:idx_player_game_adv" json:"gameId"` + PlayerID string `gorm:"not null;uniqueIndex:idx_player_game_adv" json:"playerId"` + PlayerName string `json:"playerName"` + Team string `gorm:"not null" json:"team"` + MP string `json:"mp"` // Minutes Played + TSPercent float64 `json:"tsPercent"` EFGPercent float64 `json:"efgPercent"` - ThreePAr float64 `json:"threePAr"` - FTr float64 `json:"fTr"` + ThreePAr float64 `json:"threePAr"` + FTr float64 `json:"fTr"` ORBPercent float64 `json:"orbPercent"` DRBPercent float64 `json:"drbPercent"` TRBPercent float64 `json:"trbPercent"` @@ -108,9 +106,9 @@ type PlayerGameAdvStat struct { BLKPercent float64 `json:"blkPercent"` TOVPercent float64 `json:"tovPercent"` USGPercent float64 `json:"usgPercent"` - ORtg int `json:"oRtg"` - DRtg int `json:"dRtg"` - BPM float64 `json:"bpm"` // Box Plus/Minus + ORtg int `json:"oRtg"` + DRtg int `json:"dRtg"` + BPM float64 `json:"bpm"` // Box Plus/Minus CreatedAt time.Time `swaggerignore:"true"` UpdatedAt time.Time `swaggerignore:"true"` @@ -119,28 +117,28 @@ type PlayerGameAdvStat struct { // TeamGameBasicStat holds the total basic stats for a team in a single game. type TeamGameBasicStat struct { - ID uint `gorm:"primaryKey" swaggerignore:"true"` - GameID string `gorm:"not null;uniqueIndex:idx_team_game_basic" json:"gameId"` - Team string `gorm:"not null;uniqueIndex:idx_team_game_basic" json:"team"` - MP int `json:"mp"` // Total minutes, usually 240 for a regulation game - FG int `json:"fg"` - FGA int `json:"fga"` - FGPercent float64 `json:"fgPercent"` - ThreeP int `json:"threeP"` - ThreePA int `json:"threePa"` + ID uint `gorm:"primaryKey" swaggerignore:"true"` + GameID string `gorm:"not null;uniqueIndex:idx_team_game_basic" json:"gameId"` + Team string `gorm:"not null;uniqueIndex:idx_team_game_basic" json:"team"` + MP int `json:"mp"` // Total minutes, usually 240 for a regulation game + FG int `json:"fg"` + FGA int `json:"fga"` + FGPercent float64 `json:"fgPercent"` + ThreeP int `json:"threeP"` + ThreePA int `json:"threePa"` ThreePPercent float64 `json:"threePPercent"` - FT int `json:"ft"` - FTA int `json:"fta"` - FTPercent float64 `json:"ftPercent"` - ORB int `json:"orb"` - DRB int `json:"drb"` - TRB int `json:"trb"` - AST int `json:"ast"` - STL int `json:"stl"` - BLK int `json:"blk"` - TOV int `json:"tov"` - PF int `json:"pf"` - PTS int `json:"pts"` + FT int `json:"ft"` + FTA int `json:"fta"` + FTPercent float64 `json:"ftPercent"` + ORB int `json:"orb"` + DRB int `json:"drb"` + TRB int `json:"trb"` + AST int `json:"ast"` + STL int `json:"stl"` + BLK int `json:"blk"` + TOV int `json:"tov"` + PF int `json:"pf"` + PTS int `json:"pts"` CreatedAt time.Time `swaggerignore:"true"` UpdatedAt time.Time `swaggerignore:"true"` @@ -149,14 +147,14 @@ type TeamGameBasicStat struct { // TeamGameAdvStat holds the total advanced stats for a team in a single game. type TeamGameAdvStat struct { - ID uint `gorm:"primaryKey" swaggerignore:"true"` - GameID string `gorm:"not null;uniqueIndex:idx_team_game_adv" json:"gameId"` - Team string `gorm:"not null;uniqueIndex:idx_team_game_adv" json:"team"` - MP int `json:"mp"` - TSPercent float64 `json:"tsPercent"` + ID uint `gorm:"primaryKey" swaggerignore:"true"` + GameID string `gorm:"not null;uniqueIndex:idx_team_game_adv" json:"gameId"` + Team string `gorm:"not null;uniqueIndex:idx_team_game_adv" json:"team"` + MP int `json:"mp"` + TSPercent float64 `json:"tsPercent"` EFGPercent float64 `json:"efgPercent"` - ThreePAr float64 `json:"threePAr"` - FTr float64 `json:"fTr"` + ThreePAr float64 `json:"threePAr"` + FTr float64 `json:"fTr"` ORBPercent float64 `json:"orbPercent"` DRBPercent float64 `json:"drbPercent"` TRBPercent float64 `json:"trbPercent"` @@ -165,8 +163,8 @@ type TeamGameAdvStat struct { BLKPercent float64 `json:"blkPercent"` TOVPercent float64 `json:"tovPercent"` USGPercent float64 `json:"usgPercent"` // Will be ~100.0 for a team - ORtg float64 `json:"oRtg"` - DRtg float64 `json:"dRtg"` + ORtg float64 `json:"oRtg"` + DRtg float64 `json:"dRtg"` CreatedAt time.Time `swaggerignore:"true"` UpdatedAt time.Time `swaggerignore:"true"` diff --git a/services/box_score_scrape_service.go b/services/box_score_scrape_service.go new file mode 100644 index 0000000..45932a6 --- /dev/null +++ b/services/box_score_scrape_service.go @@ -0,0 +1,334 @@ +// File: services/box_score_scrape_service.go +package services + +import ( + "fmt" + "log" + "net/http" + "strings" + "time" + + "github.com/PuerkitoBio/goquery" + "github.com/nprasad2077/NBA_Go/models" + "github.com/nprasad2077/NBA_Go/utils" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +const boxScoreURLBase = "https://www.basketball-reference.com" + +// FetchAndStoreBoxScoreDataForDateRange fetches all games in a date range and scrapes their box scores. +func FetchAndStoreBoxScoreDataForDateRange(db *gorm.DB, from, to time.Time) error { + var games []models.Game + // Query the database for games within the specified date range. + if err := db.Where("date >= ? AND date < ?", from, to).Find(&games).Error; err != nil { + return fmt.Errorf("failed to query games from DB: %w", err) + } + + log.Printf("Found %d games to process in the specified date range.", len(games)) + + for _, game := range games { + log.Printf("Processing game: %s", game.GameID) + fullURL := boxScoreURLBase + game.BoxScoreURL + if err := scrapeBoxScorePage(db, fullURL, game.GameID); err != nil { + // Log the error but continue to the next game + log.Printf("Error processing box score for game %s: %v", game.GameID, err) + } + // Be a good internet citizen and pause between requests. + utils.SleepWithJitter(1500 * time.Millisecond) + } + return nil +} + +// scrapeBoxScorePage handles fetching and parsing a single box score page. +func scrapeBoxScorePage(db *gorm.DB, url, gameID string) error { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("received non-200 status code: %s", resp.Status) + } + + doc, err := goquery.NewDocumentFromReader(resp.Body) + if err != nil { + return err + } + + // The content is often inside a comment, so we need to extract it. + doc = uncommentDoc(doc) + + // --- Scrape all data types from the page --- + if err := parseAndStoreLineScore(db, doc, gameID); err != nil { + return err + } + if err := parseAndStoreBoxScores(db, doc, gameID); err != nil { + return err + } + + return nil +} + +// parseAndStoreLineScore scrapes the line score table. +func parseAndStoreLineScore(db *gorm.DB, doc *goquery.Document, gameID string) error { + var lineScores []models.LineScore + doc.Find("table#line_score tbody tr").Each(func(i int, row *goquery.Selection) { + ls := models.LineScore{GameID: gameID} + ls.Team = row.Find(`th[data-stat="team"] a`).Text() + ls.Q1 = mustAtoi(row.Find(`td[data-stat="1"]`).Text()) + ls.Q2 = mustAtoi(row.Find(`td[data-stat="2"]`).Text()) + ls.Q3 = mustAtoi(row.Find(`td[data-stat="3"]`).Text()) + ls.Q4 = mustAtoi(row.Find(`td[data-stat="4"]`).Text()) + ls.OT1 = mustAtoi(row.Find(`td[data-stat="OT1"]`).Text()) + ls.OT2 = mustAtoi(row.Find(`td[data-stat="OT2"]`).Text()) + ls.OT3 = mustAtoi(row.Find(`td[data-stat="OT3"]`).Text()) + ls.Total = mustAtoi(row.Find(`td[data-stat="T"]`).Text()) + lineScores = append(lineScores, ls) + }) + + if len(lineScores) > 0 { + return db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "game_id"}, {Name: "team"}}, + DoUpdates: clause.AssignmentColumns([]string{"q1", "q2", "q3", "q4", "ot1", "ot2", "ot3", "total"}), + }).Create(&lineScores).Error + } + return nil +} + +// parseAndStoreBoxScores finds all basic and advanced box score tables and processes them. +func parseAndStoreBoxScores(db *gorm.DB, doc *goquery.Document, gameID string) error { + var allPlayerBasicStats []models.PlayerGameBasicStat + var allPlayerAdvStats []models.PlayerGameAdvStat + var allTeamBasicStats []models.TeamGameBasicStat + var allTeamAdvStats []models.TeamGameAdvStat + + // Use a CSS attribute selector to find all box score tables for both teams. + doc.Find(`table[id^="box-"][id$="-game-basic"], table[id^="box-"][id$="-game-advanced"]`).Each(func(i int, table *goquery.Selection) { + tableID, _ := table.Attr("id") + isAdvanced := strings.Contains(tableID, "-advanced") + teamAbbr := strings.TrimSuffix(strings.TrimPrefix(tableID, "box-"), "-game-basic") + teamAbbr = strings.TrimSuffix(teamAbbr, "-game-advanced") + + // Process player rows + table.Find("tbody tr").Each(func(j int, row *goquery.Selection) { + playerID, exists := row.Find("th").Attr("data-append-csv") + if !exists || playerID == "" { + return // Not a player row + } + + // Handle "Did Not Play" or other statuses + reason := row.Find(`td[data-stat="reason"]`) + status := "Played" + if reason.Length() > 0 { + status = reason.Text() + } + + if !isAdvanced { + stat := parsePlayerBasicStat(row, gameID, playerID, teamAbbr, status) + allPlayerBasicStats = append(allPlayerBasicStats, stat) + } else { + stat := parsePlayerAdvStat(row, gameID, playerID, teamAbbr, status) + allPlayerAdvStats = append(allPlayerAdvStats, stat) + } + }) + + // Process team total row + table.Find("tfoot tr").Each(func(j int, row *goquery.Selection) { + if !isAdvanced { + stat := parseTeamBasicStat(row, gameID, teamAbbr) + allTeamBasicStats = append(allTeamBasicStats, stat) + } else { + stat := parseTeamAdvStat(row, gameID, teamAbbr) + allTeamAdvStats = append(allTeamAdvStats, stat) + } + }) + }) + + // Batch upsert all collected stats + if err := batchUpsertAll(db, allPlayerBasicStats, allPlayerAdvStats, allTeamBasicStats, allTeamAdvStats); err != nil { + return err + } + + return nil +} + +// --- Parsing Helper Functions --- + +func parsePlayerBasicStat(row *goquery.Selection, gameID, playerID, team, status string) models.PlayerGameBasicStat { + return models.PlayerGameBasicStat{ + GameID: gameID, + PlayerID: playerID, + PlayerName: row.Find(`th[data-stat="player"] a`).Text(), + Team: team, + Status: status, + MP: row.Find(`td[data-stat="mp"]`).Text(), + FG: mustAtoi(row.Find(`td[data-stat="fg"]`).Text()), + FGA: mustAtoi(row.Find(`td[data-stat="fga"]`).Text()), + FGPercent: mustParseFloat(row.Find(`td[data-stat="fg_pct"]`).Text()), + ThreeP: mustAtoi(row.Find(`td[data-stat="fg3"]`).Text()), + ThreePA: mustAtoi(row.Find(`td[data-stat="fg3a"]`).Text()), + ThreePPercent: mustParseFloat(row.Find(`td[data-stat="fg3_pct"]`).Text()), + FT: mustAtoi(row.Find(`td[data-stat="ft"]`).Text()), + FTA: mustAtoi(row.Find(`td[data-stat="fta"]`).Text()), + FTPercent: mustParseFloat(row.Find(`td[data-stat="ft_pct"]`).Text()), + ORB: mustAtoi(row.Find(`td[data-stat="orb"]`).Text()), + DRB: mustAtoi(row.Find(`td[data-stat="drb"]`).Text()), + TRB: mustAtoi(row.Find(`td[data-stat="trb"]`).Text()), + AST: mustAtoi(row.Find(`td[data-stat="ast"]`).Text()), + STL: mustAtoi(row.Find(`td[data-stat="stl"]`).Text()), + BLK: mustAtoi(row.Find(`td[data-stat="blk"]`).Text()), + TOV: mustAtoi(row.Find(`td[data-stat="tov"]`).Text()), + PF: mustAtoi(row.Find(`td[data-stat="pf"]`).Text()), + PTS: mustAtoi(row.Find(`td[data-stat="pts"]`).Text()), + GmSc: mustParseFloat(row.Find(`td[data-stat="game_score"]`).Text()), + PlusMinus: mustAtoiWithSign(row.Find(`td[data-stat="plus_minus"]`).Text()), + } +} + +func parsePlayerAdvStat(row *goquery.Selection, gameID, playerID, team, status string) models.PlayerGameAdvStat { + return models.PlayerGameAdvStat{ + GameID: gameID, + PlayerID: playerID, + PlayerName: row.Find(`th[data-stat="player"] a`).Text(), + Team: team, + MP: row.Find(`td[data-stat="mp"]`).Text(), + TSPercent: mustParseFloat(row.Find(`td[data-stat="ts_pct"]`).Text()), + EFGPercent: mustParseFloat(row.Find(`td[data-stat="efg_pct"]`).Text()), + ThreePAr: mustParseFloat(row.Find(`td[data-stat="fg3a_per_fga_pct"]`).Text()), + FTr: mustParseFloat(row.Find(`td[data-stat="fta_per_fga_pct"]`).Text()), + ORBPercent: mustParseFloat(row.Find(`td[data-stat="orb_pct"]`).Text()), + DRBPercent: mustParseFloat(row.Find(`td[data-stat="drb_pct"]`).Text()), + TRBPercent: mustParseFloat(row.Find(`td[data-stat="trb_pct"]`).Text()), + ASTPercent: mustParseFloat(row.Find(`td[data-stat="ast_pct"]`).Text()), + STLPercent: mustParseFloat(row.Find(`td[data-stat="stl_pct"]`).Text()), + BLKPercent: mustParseFloat(row.Find(`td[data-stat="blk_pct"]`).Text()), + TOVPercent: mustParseFloat(row.Find(`td[data-stat="tov_pct"]`).Text()), + USGPercent: mustParseFloat(row.Find(`td[data-stat="usg_pct"]`).Text()), + ORtg: mustAtoi(row.Find(`td[data-stat="off_rtg"]`).Text()), + DRtg: mustAtoi(row.Find(`td[data-stat="def_rtg"]`).Text()), + BPM: mustParseFloat(row.Find(`td[data-stat="bpm"]`).Text()), + } +} + +func parseTeamBasicStat(row *goquery.Selection, gameID, team string) models.TeamGameBasicStat { + return models.TeamGameBasicStat{ + GameID: gameID, + Team: team, + MP: mustAtoi(row.Find(`td[data-stat="mp"]`).Text()), + FG: mustAtoi(row.Find(`td[data-stat="fg"]`).Text()), + FGA: mustAtoi(row.Find(`td[data-stat="fga"]`).Text()), + FGPercent: mustParseFloat(row.Find(`td[data-stat="fg_pct"]`).Text()), + ThreeP: mustAtoi(row.Find(`td[data-stat="fg3"]`).Text()), + ThreePA: mustAtoi(row.Find(`td[data-stat="fg3a"]`).Text()), + ThreePPercent: mustParseFloat(row.Find(`td[data-stat="fg3_pct"]`).Text()), + FT: mustAtoi(row.Find(`td[data-stat="ft"]`).Text()), + FTA: mustAtoi(row.Find(`td[data-stat="fta"]`).Text()), + FTPercent: mustParseFloat(row.Find(`td[data-stat="ft_pct"]`).Text()), + ORB: mustAtoi(row.Find(`td[data-stat="orb"]`).Text()), + DRB: mustAtoi(row.Find(`td[data-stat="drb"]`).Text()), + TRB: mustAtoi(row.Find(`td[data-stat="trb"]`).Text()), + AST: mustAtoi(row.Find(`td[data-stat="ast"]`).Text()), + STL: mustAtoi(row.Find(`td[data-stat="stl"]`).Text()), + BLK: mustAtoi(row.Find(`td[data-stat="blk"]`).Text()), + TOV: mustAtoi(row.Find(`td[data-stat="tov"]`).Text()), + PF: mustAtoi(row.Find(`td[data-stat="pf"]`).Text()), + PTS: mustAtoi(row.Find(`td[data-stat="pts"]`).Text()), + } +} + +func parseTeamAdvStat(row *goquery.Selection, gameID, team string) models.TeamGameAdvStat { + return models.TeamGameAdvStat{ + GameID: gameID, + Team: team, + MP: mustAtoi(row.Find(`td[data-stat="mp"]`).Text()), + TSPercent: mustParseFloat(row.Find(`td[data-stat="ts_pct"]`).Text()), + EFGPercent: mustParseFloat(row.Find(`td[data-stat="efg_pct"]`).Text()), + ThreePAr: mustParseFloat(row.Find(`td[data-stat="fg3a_per_fga_pct"]`).Text()), + FTr: mustParseFloat(row.Find(`td[data-stat="fta_per_fga_pct"]`).Text()), + ORBPercent: mustParseFloat(row.Find(`td[data-stat="orb_pct"]`).Text()), + DRBPercent: mustParseFloat(row.Find(`td[data-stat="drb_pct"]`).Text()), + TRBPercent: mustParseFloat(row.Find(`td[data-stat="trb_pct"]`).Text()), + ASTPercent: mustParseFloat(row.Find(`td[data-stat="ast_pct"]`).Text()), + STLPercent: mustParseFloat(row.Find(`td[data-stat="stl_pct"]`).Text()), + BLKPercent: mustParseFloat(row.Find(`td[data-stat="blk_pct"]`).Text()), + TOVPercent: mustParseFloat(row.Find(`td[data-stat="tov_pct"]`).Text()), + USGPercent: mustParseFloat(row.Find(`td[data-stat="usg_pct"]`).Text()), + ORtg: mustParseFloat(row.Find(`td[data-stat="off_rtg"]`).Text()), + DRtg: mustParseFloat(row.Find(`td[data-stat="def_rtg"]`).Text()), + } +} + +// --- DB and Utility Functions --- + +func batchUpsertAll(db *gorm.DB, pbs []models.PlayerGameBasicStat, pas []models.PlayerGameAdvStat, tbs []models.TeamGameBasicStat, tas []models.TeamGameAdvStat) error { + if len(pbs) > 0 { + if err := db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "game_id"}, {Name: "player_id"}}, + DoUpdates: clause.AssignmentColumns(getModelColumns(&models.PlayerGameBasicStat{})), + }).Create(&pbs).Error; err != nil { + return fmt.Errorf("failed to upsert player basic stats: %w", err) + } + } + if len(pas) > 0 { + if err := db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "game_id"}, {Name: "player_id"}}, + DoUpdates: clause.AssignmentColumns(getModelColumns(&models.PlayerGameAdvStat{})), + }).Create(&pas).Error; err != nil { + return fmt.Errorf("failed to upsert player advanced stats: %w", err) + } + } + if len(tbs) > 0 { + if err := db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "game_id"}, {Name: "team"}}, + DoUpdates: clause.AssignmentColumns(getModelColumns(&models.TeamGameBasicStat{})), + }).Create(&tbs).Error; err != nil { + return fmt.Errorf("failed to upsert team basic stats: %w", err) + } + } + if len(tas) > 0 { + if err := db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "game_id"}, {Name: "team"}}, + DoUpdates: clause.AssignmentColumns(getModelColumns(&models.TeamGameAdvStat{})), + }).Create(&tas).Error; err != nil { + return fmt.Errorf("failed to upsert team advanced stats: %w", err) + } + } + return nil +} + +// uncommentDoc finds and replaces commented out HTML sections. +func uncommentDoc(doc *goquery.Document) *goquery.Document { + doc.Find("#content").Find(".placeholder, .section_heading").Each(func(i int, s *goquery.Selection) { + s.NextUntil(".placeholder, .section_heading").FilterFunction(func(i int, s *goquery.Selection) bool { + return goquery.NodeName(s) == "#comment" + }).Each(func(i int, s *goquery.Selection) { + uncommented, _ := goquery.NewDocumentFromReader(strings.NewReader(s.Text())) + // FIX: Use ReplaceWithSelection, as the argument is a goquery selection, not a string. + s.ReplaceWithSelection(uncommented.Find("body").Children()) + }) + }) + return doc +} + +// getModelColumns is a placeholder for a more robust reflection-based column name generator. +// For now, it returns hardcoded lists. +func getModelColumns(model interface{}) []string { + switch model.(type) { + case *models.PlayerGameBasicStat: + return []string{"player_name", "team", "status", "mp", "fg", "fga", "fg_percent", "three_p", "three_pa", "three_p_percent", "ft", "fta", "ft_percent", "orb", "drb", "trb", "ast", "stl", "blk", "tov", "pf", "pts", "gm_sc", "plus_minus"} + case *models.PlayerGameAdvStat: + return []string{"player_name", "team", "mp", "ts_percent", "efg_percent", "three_p_ar", "f_tr", "orb_percent", "drb_percent", "trb_percent", "ast_percent", "stl_percent", "blk_percent", "tov_percent", "usg_percent", "o_rtg", "d_rtg", "bpm"} + case *models.TeamGameBasicStat: + return []string{"mp", "fg", "fga", "fg_percent", "three_p", "three_pa", "three_p_percent", "ft", "fta", "ft_percent", "orb", "drb", "trb", "ast", "stl", "blk", "tov", "pf", "pts"} + case *models.TeamGameAdvStat: + return []string{"mp", "ts_percent", "efg_percent", "three_p_ar", "f_tr", "orb_percent", "drb_percent", "trb_percent", "ast_percent", "stl_percent", "blk_percent", "tov_percent", "usg_percent", "o_rtg", "d_rtg"} + } + return []string{} +} diff --git a/services/game_scrape_service.go b/services/game_scrape_service.go new file mode 100644 index 0000000..386db98 --- /dev/null +++ b/services/game_scrape_service.go @@ -0,0 +1,190 @@ +// File: services/game_scrape_service.go +package services + +import ( + "bytes" + "fmt" + "io" + "log" + "net/http" + "strings" + "time" + + "github.com/PuerkitoBio/goquery" + "github.com/nprasad2077/NBA_Go/models" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +const gameScheduleURLFmt = "https://www.basketball-reference.com/leagues/NBA_%d_games-%s.html" + +// FetchAndStoreGameSchedule scrapes the game schedule for a given season and month. +// The month should be the full lowercase name, e.g., "october", "november". +// If db is nil, it will perform a "dry run" and print the parsed data to the console. +func FetchAndStoreGameSchedule(db *gorm.DB, season int, month string) error { + url := fmt.Sprintf(gameScheduleURLFmt, season, month) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("failed to fetch schedule for %s %d: %w", month, season, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + log.Printf("⚠️ Skipping schedule for %s %d (Status: %s)", month, season, resp.Status) + return nil // Not a fatal error, just no data for this month. + } + + htmlBytes, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body for %s %d: %w", month, season, err) + } + + doc, err := goquery.NewDocumentFromReader(bytes.NewReader(htmlBytes)) + if err != nil { + return fmt.Errorf("failed to parse HTML for %s %d: %w", month, season, err) + } + + table := doc.Find("table#schedule") + if table.Length() == 0 { + // Sometimes the content is commented out + commentNode := doc.Find("#all_schedule").Contents().FilterFunction(func(i int, s *goquery.Selection) bool { + return goquery.NodeName(s) == "#comment" + }) + if commentNode.Length() > 0 { + commentedHTML := commentNode.Nodes[0].FirstChild.Data + innerDoc, err := goquery.NewDocumentFromReader(strings.NewReader(commentedHTML)) + if err != nil { + return fmt.Errorf("failed to parse commented schedule HTML: %w", err) + } + table = innerDoc.Find("table#schedule") + } + } + + if table.Length() == 0 { + log.Printf("No schedule table found for %s %d.", month, season) + return nil + } + + var gamesToUpsert []models.Game + table.Find("tbody tr").Each(func(i int, row *goquery.Selection) { + // Skip table header rows that are sometimes repeated in the body + if row.Find("th.poptip").Length() > 1 { + return + } + + // Skip rows that don't represent games (e.g., placeholder rows) + if row.Find(`[data-stat="visitor_team_name"]`).Text() == "" { + return + } + + var game models.Game + var gameID string + + // Extract GameID from the box score link, which is the most reliable unique key + boxScoreCell := row.Find(`td[data-stat="box_score_text"] a`) + if href, exists := boxScoreCell.Attr("href"); exists { + parts := strings.Split(href, "/") + fileName := parts[len(parts)-1] + gameID = strings.TrimSuffix(fileName, ".html") + } + + // If there's no box score link, it's likely a future game, we can skip it or handle differently + if gameID == "" { + return + } + game.GameID = gameID + + // Get the date part from the 'csk' attribute for accuracy + dateCsk, _ := row.Find(`th[data-stat="date_game"]`).Attr("csk") + var datePart string + if len(dateCsk) >= 8 { + datePart = dateCsk[:8] + } else { + log.Printf("Could not parse date from invalid 'csk' attribute: %s. Skipping.", dateCsk) + return + } + + // Get the start time string + startTimeET := row.Find(`td[data-stat="game_start_time"]`).Text() + if startTimeET == "" { + // Skip games without a start time, as they can't be parsed accurately + log.Printf("Could not find start time for game %s. Skipping.", gameID) + return + } + + // Load the US/Eastern timezone to correctly handle ET/EST/EDT + eastern, err := time.LoadLocation("America/New_York") + if err != nil { + log.Printf("FATAL: Could not load America/New_York timezone: %v", err) + // This is a system-level error, so we stop the row processing here. + // The function will continue and process any games already parsed. + return + } + + // Combine date and time and parse together in the correct timezone. + // The layout "3:04p" handles times like "7:30p". + fullDateTimeString := datePart + startTimeET + layout := "200601023:04p" + gameDate, err := time.ParseInLocation(layout, fullDateTimeString, eastern) + if err != nil { + log.Printf("Could not parse combined date-time for game %s (value: '%s'): %v. Skipping.", gameID, fullDateTimeString, err) + return + } + game.Date = gameDate + game.StartTimeET = startTimeET // Keep the original string as well + + game.VisitorTeam = row.Find(`td[data-stat="visitor_team_name"] a`).Text() + game.VisitorPTS = mustAtoi(row.Find(`td[data-stat="visitor_pts"]`).Text()) + game.HomeTeam = row.Find(`td[data-stat="home_team_name"] a`).Text() + game.HomePTS = mustAtoi(row.Find(`td[data-stat="home_pts"]`).Text()) + game.BoxScoreURL, _ = boxScoreCell.Attr("href") + game.GameDuration = row.Find(`td[data-stat="game_duration"]`).Text() + game.Arena = row.Find(`td[data-stat="arena_name"]`).Text() + game.IsPlayoff = strings.Contains(row.Find(`td[data-stat="game_remarks"]`).Text(), "Playoffs") + + gamesToUpsert = append(gamesToUpsert, game) + }) + + if len(gamesToUpsert) > 0 { + // If the db connection is nil, we're in test/debug mode. Print to console. + if db == nil { + log.Println("--- RUNNING IN DRY-RUN MODE ---") + for _, game := range gamesToUpsert { + // Use %+v to print the struct with field names for clarity + log.Printf("Game Data: %+v\n", game) + } + log.Printf("--- WOULD INSERT %d RECORDS ---", len(gamesToUpsert)) + return nil // End execution for dry-run + } + + log.Printf("Attempting to batch upsert %d games for %s %d...", len(gamesToUpsert), month, season) + + if err := db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "game_id"}}, + DoUpdates: clause.AssignmentColumns(allGameColumns()), + }).Create(&gamesToUpsert).Error; err != nil { + log.Printf("Failed to batch upsert games: %v", err) + return err + } + log.Printf("✅ Successfully batch upserted %d game records for %s %d.", len(gamesToUpsert), month, season) + } else { + log.Printf("No game data found to import for %s %d.", month, season) + } + + return nil +} + +// allGameColumns returns a list of all column names in the Game model for the upsert operation. +// This ensures that if a record exists, all its fields are updated with the new data. +func allGameColumns() []string { + return []string{ + "date", "is_playoff", "start_time_et", "arena", "visitor_team", + "visitor_pts", "home_team", "home_pts", "game_duration", "box_score_url", + "updated_at", + } +} diff --git a/services/helpers.go b/services/helpers.go index 324676e..96f307c 100644 --- a/services/helpers.go +++ b/services/helpers.go @@ -8,6 +8,16 @@ func mustAtoi(s string) int { return i } +// mustAtoiWithSign handles strings that might have a "+" or "-" sign. +func mustAtoiWithSign(s string) int { + if s == "" { + return 0 + } + // The strconv.Atoi function handles the sign automatically. + i, _ := strconv.Atoi(s) + return i +} + // mustParseFloat parses s into a float64, or returns 0.0 on error. func mustParseFloat(s string) float64 { f, _ := strconv.ParseFloat(s, 64) From 5f7a1eb3559fbb1eca7bbbc26c6afb564cc1788a Mon Sep 17 00:00:00 2001 From: Ravi Prasad Date: Tue, 8 Jul 2025 18:55:04 -0500 Subject: [PATCH 07/10] games and related stats working except line score --- main.go | 2 +- services/box_score_scrape_service.go | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 48eadbb..af4b231 100644 --- a/main.go +++ b/main.go @@ -65,7 +65,7 @@ func main() { log.Println("🎉 Game Imports completed successfully 🏀") importBoxScores(db) - log.Println("🎉 Related Box-Score Imports completed successfully 📦") + log.Println("🎉 Related Box Score Imports completed successfully 📦") log.Println("🏀 ALL Imports completed successfully ✅ 🙌") diff --git a/services/box_score_scrape_service.go b/services/box_score_scrape_service.go index 45932a6..96c66f0 100644 --- a/services/box_score_scrape_service.go +++ b/services/box_score_scrape_service.go @@ -86,9 +86,10 @@ func parseAndStoreLineScore(db *gorm.DB, doc *goquery.Document, gameID string) e ls.Q2 = mustAtoi(row.Find(`td[data-stat="2"]`).Text()) ls.Q3 = mustAtoi(row.Find(`td[data-stat="3"]`).Text()) ls.Q4 = mustAtoi(row.Find(`td[data-stat="4"]`).Text()) - ls.OT1 = mustAtoi(row.Find(`td[data-stat="OT1"]`).Text()) - ls.OT2 = mustAtoi(row.Find(`td[data-stat="OT2"]`).Text()) - ls.OT3 = mustAtoi(row.Find(`td[data-stat="OT3"]`).Text()) + // FIX: Corrected the data-stat attribute for overtime periods. + ls.OT1 = mustAtoi(row.Find(`td[data-stat="1OT"]`).Text()) + ls.OT2 = mustAtoi(row.Find(`td[data-stat="2OT"]`).Text()) + ls.OT3 = mustAtoi(row.Find(`td[data-stat="3OT"]`).Text()) ls.Total = mustAtoi(row.Find(`td[data-stat="T"]`).Text()) lineScores = append(lineScores, ls) }) From fe1379e54e0a5074aea429caac93a815293d58d0 Mon Sep 17 00:00:00 2001 From: Ravi Prasad Date: Tue, 8 Jul 2025 20:20:39 -0500 Subject: [PATCH 08/10] line score service isolated and working --- models/game.go | 4 +- services/box_score_scrape_service.go | 67 ++++++----------- services/line_score_scrape_service.go | 54 +++++++++++++ test/run_scraper.go | 104 ++++++++++++++++++++++++++ 4 files changed, 182 insertions(+), 47 deletions(-) create mode 100644 services/line_score_scrape_service.go create mode 100644 test/run_scraper.go diff --git a/models/game.go b/models/game.go index 6ca8ff7..79261f7 100644 --- a/models/game.go +++ b/models/game.go @@ -34,8 +34,8 @@ type Game struct { // LineScore holds the scoring for each team by quarter for a specific game. type LineScore struct { ID uint `gorm:"primaryKey" swaggerignore:"true"` - GameID string `gorm:"not null;index" json:"gameId"` - Team string `gorm:"not null;index" json:"team"` + GameID string `gorm:"not null;uniqueIndex:idx_line_score,priority:1" json:"gameId"` + Team string `gorm:"not null;uniqueIndex:idx_line_score,priority:2" json:"team"` Q1 int `json:"q1"` Q2 int `json:"q2"` Q3 int `json:"q3"` diff --git a/services/box_score_scrape_service.go b/services/box_score_scrape_service.go index 96c66f0..1f098df 100644 --- a/services/box_score_scrape_service.go +++ b/services/box_score_scrape_service.go @@ -1,4 +1,3 @@ -// File: services/box_score_scrape_service.go package services import ( @@ -17,6 +16,21 @@ import ( const boxScoreURLBase = "https://www.basketball-reference.com" +// uncommentDoc finds and replaces commented out HTML sections. +func uncommentDoc(doc *goquery.Document) *goquery.Document { + doc.Find("*").Contents().FilterFunction(func(i int, s *goquery.Selection) bool { + return goquery.NodeName(s) == "#comment" + }).Each(func(i int, s *goquery.Selection) { + // Use .Data on the underlying html.Node to get the comment content. + commentText := s.Nodes[0].Data + if strings.Contains(commentText, " 0 { - return db.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "game_id"}, {Name: "team"}}, - DoUpdates: clause.AssignmentColumns([]string{"q1", "q2", "q3", "q4", "ot1", "ot2", "ot3", "total"}), - }).Create(&lineScores).Error - } - return nil -} - // parseAndStoreBoxScores finds all basic and advanced box score tables and processes them. func parseAndStoreBoxScores(db *gorm.DB, doc *goquery.Document, gameID string) error { var allPlayerBasicStats []models.PlayerGameBasicStat @@ -304,19 +293,7 @@ func batchUpsertAll(db *gorm.DB, pbs []models.PlayerGameBasicStat, pas []models. return nil } -// uncommentDoc finds and replaces commented out HTML sections. -func uncommentDoc(doc *goquery.Document) *goquery.Document { - doc.Find("#content").Find(".placeholder, .section_heading").Each(func(i int, s *goquery.Selection) { - s.NextUntil(".placeholder, .section_heading").FilterFunction(func(i int, s *goquery.Selection) bool { - return goquery.NodeName(s) == "#comment" - }).Each(func(i int, s *goquery.Selection) { - uncommented, _ := goquery.NewDocumentFromReader(strings.NewReader(s.Text())) - // FIX: Use ReplaceWithSelection, as the argument is a goquery selection, not a string. - s.ReplaceWithSelection(uncommented.Find("body").Children()) - }) - }) - return doc -} + // getModelColumns is a placeholder for a more robust reflection-based column name generator. // For now, it returns hardcoded lists. diff --git a/services/line_score_scrape_service.go b/services/line_score_scrape_service.go new file mode 100644 index 0000000..7f6f4a4 --- /dev/null +++ b/services/line_score_scrape_service.go @@ -0,0 +1,54 @@ +// File: services/line_score_scrape_service.go +package services + +import ( + "log" + + "github.com/PuerkitoBio/goquery" + "github.com/nprasad2077/NBA_Go/models" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +// FetchAndStoreLineScore parses the line score table from a goquery document and upserts it to the DB. +// It assumes the document has already been processed to make commented-out tables visible. +func FetchAndStoreLineScore(db *gorm.DB, doc *goquery.Document, gameID string) error { + var lineScores []models.LineScore + + doc.Find("table#line_score tbody tr").Each(func(i int, row *goquery.Selection) { + teamName := row.Find(`th[data-stat="team"] a`).Text() + if teamName == "" { + return // Skip invalid rows + } + + ls := models.LineScore{ + GameID: gameID, + Team: teamName, + } + ls.Q1 = mustAtoi(row.Find(`td[data-stat="1"]`).Text()) + ls.Q2 = mustAtoi(row.Find(`td[data-stat="2"]`).Text()) + ls.Q3 = mustAtoi(row.Find(`td[data-stat="3"]`).Text()) + ls.Q4 = mustAtoi(row.Find(`td[data-stat="4"]`).Text()) + ls.OT1 = mustAtoi(row.Find(`td[data-stat="OT1"]`).Text()) + ls.OT2 = mustAtoi(row.Find(`td[data-stat="OT2"]`).Text()) + ls.OT3 = mustAtoi(row.Find(`td[data-stat="OT3"]`).Text()) + ls.Total = mustAtoi(row.Find(`td[data-stat="T"]`).Text()) + lineScores = append(lineScores, ls) + }) + + if len(lineScores) == 0 { + log.Printf("No line score data found to import for game %s.", gameID) + return nil + } + + // Upsert the data into the database. + if err := db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "game_id"}, {Name: "team"}}, + DoUpdates: clause.AssignmentColumns([]string{"q1", "q2", "q3", "q4", "ot1", "ot2", "ot3", "total"}), + }).Create(&lineScores).Error; err != nil { + return err + } + + log.Printf("Successfully processed line scores for game %s.", gameID) + return nil +} \ No newline at end of file diff --git a/test/run_scraper.go b/test/run_scraper.go new file mode 100644 index 0000000..5238f73 --- /dev/null +++ b/test/run_scraper.go @@ -0,0 +1,104 @@ +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "strconv" + "strings" + + "github.com/PuerkitoBio/goquery" +) + +// LineScore struct and mustAtoi helper function remain the same +type LineScore struct { + GameID string + Team string + Q1, Q2, Q3, Q4 int + OT1, OT2, OT3 int + Total int +} +func mustAtoi(s string) int { i, _ := strconv.Atoi(s); return i } + + +func scrapeAndPrintLineScore(gameURL, gameID string) { + // 1. Fetch the page + resp, err := http.Get(gameURL) + if err != nil { + log.Fatalf("Failed to fetch page: %v", err) + } + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Failed to read response body: %v", err) + } + + doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(bodyBytes))) + if err != nil { + log.Fatalf("Failed to parse HTML: %v", err) + } + + var table *goquery.Selection + + // Try to find a visible table first + table = doc.Find("table#line_score") + + if table.Length() == 0 { + log.Println("Visible table not found. Finding and parsing commented table...") + + commentNode := doc.Find("#all_line_score").Contents().FilterFunction(func(i int, s *goquery.Selection) bool { + return goquery.NodeName(s) == "#comment" + }) + + if commentNode.Length() > 0 { + // ✅ THE FIX: Access the .Data field directly from the comment node. + commentedHTML := commentNode.Nodes[0].Data + + innerDoc, err := goquery.NewDocumentFromReader(strings.NewReader(commentedHTML)) + if err != nil { + log.Fatalf("❌ Failed to parse the HTML from the comment: %v", err) + } + table = innerDoc.Find("table#line_score") + } + } + + if table == nil || table.Length() == 0 { + log.Println("❌ No line score table found by any method.") + return + } + + // 3. Parse the data from the found table + var lineScores []LineScore + table.Find("tbody tr").Each(func(i int, row *goquery.Selection) { + teamName := row.Find(`th[data-stat="team"] a`).Text() + if teamName != "" { + ls := LineScore{GameID: gameID, Team: teamName} + ls.Q1 = mustAtoi(row.Find(`td[data-stat="1"]`).Text()) + ls.Q2 = mustAtoi(row.Find(`td[data-stat="2"]`).Text()) + ls.Q3 = mustAtoi(row.Find(`td[data-stat="3"]`).Text()) + ls.Q4 = mustAtoi(row.Find(`td[data-stat="4"]`).Text()) + ls.Total = mustAtoi(row.Find(`td[data-stat="T"]`).Text()) + lineScores = append(lineScores, ls) + } + }) + + // 4. Print the results + if len(lineScores) > 0 { + log.Printf("✅ Success! Found %d line scores for game %s:", len(lineScores), gameID) + for _, ls := range lineScores { + log.Printf("%+v\n", ls) + } + } else { + log.Println("❌ Table was found, but failed to parse any rows.") + } +} + +func main() { + gameID := "202406170BOS" + gameURL := fmt.Sprintf("https://www.basketball-reference.com/boxscores/%s.html", gameID) + log.Printf("--- Scraping Line Score for Game: %s ---", gameID) + log.Printf("--- URL: %s ---", gameURL) + scrapeAndPrintLineScore(gameURL, gameID) +} \ No newline at end of file From bec96a1578d6fa91e43bb6114bbc8b86193979db Mon Sep 17 00:00:00 2001 From: Ravi Prasad Date: Tue, 8 Jul 2025 23:15:32 -0500 Subject: [PATCH 09/10] comment out table drops --- config/database.go | 24 +++++++-------- import.go | 46 ++++++++++++++-------------- services/box_score_scrape_service.go | 2 +- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/config/database.go b/config/database.go index 080bf78..814e8c2 100644 --- a/config/database.go +++ b/config/database.go @@ -33,18 +33,18 @@ func InitDB(shouldMigrate bool) *gorm.DB { // --- ADD THIS BLOCK TO DROP STALE TABLES --- // Drop tables in reverse order of dependency (children first). // This ensures a clean migration every time the import process runs. - log.Println("⚠️ Dropping existing game-related tables for a clean migration...") - if err := db.Migrator().DropTable( - &models.LineScore{}, - &models.PlayerGameBasicStat{}, - &models.PlayerGameAdvStat{}, - &models.TeamGameBasicStat{}, - &models.TeamGameAdvStat{}, - &models.Game{}, // Drop parent table last - ); err != nil { - log.Fatalf("failed to drop tables: %v", err) - } - log.Println("✅ Tables dropped successfully.") + // log.Println("⚠️ Dropping existing game-related tables for a clean migration...") + // if err := db.Migrator().DropTable( + // &models.LineScore{}, + // &models.PlayerGameBasicStat{}, + // &models.PlayerGameAdvStat{}, + // &models.TeamGameBasicStat{}, + // &models.TeamGameAdvStat{}, + // &models.Game{}, // Drop parent table last + // ); err != nil { + // log.Fatalf("failed to drop tables: %v", err) + // } + // log.Println("✅ Tables dropped successfully.") // --- END OF ADDED BLOCK --- diff --git a/import.go b/import.go index 4792e7d..5f6321f 100644 --- a/import.go +++ b/import.go @@ -4,9 +4,9 @@ import ( "log" "time" - "gorm.io/gorm" "github.com/nprasad2077/NBA_Go/services" "github.com/nprasad2077/NBA_Go/utils" + "gorm.io/gorm" ) // importPlayerAdvanced fetches and stores advanced stats for seasons 2017–2025 @@ -35,37 +35,39 @@ func importPlayerAdvancedPlayoffs(db *gorm.DB) { // importPlayerTotalsScrape fetches & stores scraped regular-season total stats func importPlayerTotalsScrape(db *gorm.DB) { - for season := 2025; season <= 2025; season++ { - if err := services.FetchAndStorePlayerTotalScrapedStats(db, season, false); err != nil { - log.Printf("scraped totals import failed for %d: %v", season, err) - } + for season := 2025; season <= 2025; season++ { + if err := services.FetchAndStorePlayerTotalScrapedStats(db, season, false); err != nil { + log.Printf("scraped totals import failed for %d: %v", season, err) + } log.Printf("Player Totals import for season: %d", season) - time.Sleep(1100 * time.Millisecond) + time.Sleep(1100 * time.Millisecond) utils.SleepWithJitter(1500 * time.Millisecond) - } + } } // importPlayerPlayoffsScrape fetches & stores scraped playoff total stats func importPlayerTotalsPlayoffsScrape(db *gorm.DB) { - for season := 2025; season <= 2025; season++ { - if err := services.FetchAndStorePlayerTotalScrapedStats(db, season, true); err != nil { - log.Printf("scraped playoffs import failed for %d: %v", season, err) - } + for season := 2025; season <= 2025; season++ { + if err := services.FetchAndStorePlayerTotalScrapedStats(db, season, true); err != nil { + log.Printf("scraped playoffs import failed for %d: %v", season, err) + } log.Printf("Player Playoffs Totals import for season: %d", season) - time.Sleep(1100 * time.Millisecond) + time.Sleep(1100 * time.Millisecond) utils.SleepWithJitter(1750 * time.Millisecond) - } + } } // importGameSchedules fetches and stores game schedules func importGameSchedules(db *gorm.DB) { // An NBA season typically runs from October to June months := []string{ - "october", "november", "december", "january", - "february", "march", "april", "may", "june", + // "october", "november", "december", "january", + // "february", "march", "april", "may", "june", + "december", "january", + "february", "march", "april", } - for season := 2025; season <= 2025; season++ { + for season := 2023; season <= 2024; season++ { log.Printf("--- Starting Game Schedule Import for Season: %d ---", season) for _, month := range months { // The service will print a warning and skip if a month has no data (e.g. May/June for a season not yet finished) @@ -82,14 +84,15 @@ func importGameSchedules(db *gorm.DB) { } } - // importBoxScores fetches and stores all box score data (line scores, player/team stats) // for games within a recent date range. func importBoxScores(db *gorm.DB) { // Define the date range for the import. - // This example fetches data for games in the last 90 days. - to := time.Now() - from := to.AddDate(0, -1, 1) // 0 years, -3 months, 0 days + + // The format is: time.Date(year, month, day, hour, min, sec, nsec, location) + to := time.Date(2025, time.April, 10, 0, 0, 0, 0, time.UTC) + // to := time.Now() + from := to.AddDate(0, -1, 0) // 0 years, -3 months, 0 days log.Printf("--- Starting Box Score Data Import from %s to %s ---", from.Format("2006-01-02"), to.Format("2006-01-02")) @@ -100,9 +103,6 @@ func importBoxScores(db *gorm.DB) { log.Printf("--- Finished Box Score Data Import ---") } - - - // importPlayerShotChart fetches shot-charts for every known player // func importPlayerShotChart(db *gorm.DB) { // const firstID = "hardeja01" diff --git a/services/box_score_scrape_service.go b/services/box_score_scrape_service.go index 1f098df..fb3d516 100644 --- a/services/box_score_scrape_service.go +++ b/services/box_score_scrape_service.go @@ -49,7 +49,7 @@ func FetchAndStoreBoxScoreDataForDateRange(db *gorm.DB, from, to time.Time) erro log.Printf("Error processing box score for game %s: %v", game.GameID, err) } // Be a good internet citizen and pause between requests. - utils.SleepWithJitter(1500 * time.Millisecond) + utils.SleepWithJitter(3000 * time.Millisecond) } return nil } From 75308d0ef4af96f8047539f4b44c985349ddc3a4 Mon Sep 17 00:00:00 2001 From: Ravi Prasad Date: Wed, 16 Jul 2025 22:55:33 -0500 Subject: [PATCH 10/10] import updates --- import.go | 30 ++++++++++++++-------------- services/box_score_scrape_service.go | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/import.go b/import.go index 5f6321f..13ca0ac 100644 --- a/import.go +++ b/import.go @@ -11,7 +11,7 @@ import ( // importPlayerAdvanced fetches and stores advanced stats for seasons 2017–2025 func importPlayerAdvanced(db *gorm.DB) { - for season := 2025; season <= 2025; season++ { + for season := 2024; season <= 2025; season++ { if err := services.FetchAndStorePlayerAdvancedScrapedStats(db, season, false); err != nil { log.Printf("advanced import failed for %d: %v", season, err) } @@ -23,37 +23,37 @@ func importPlayerAdvanced(db *gorm.DB) { // importPlayerAdvancedPlayoffs fetches and stores advanced stats for playoffs seasons 2023–2025 func importPlayerAdvancedPlayoffs(db *gorm.DB) { - for season := 2025; season <= 2025; season++ { + for season := 2024; season <= 2025; season++ { if err := services.FetchAndStorePlayerAdvancedScrapedStats(db, season, true); err != nil { log.Printf("advanced import failed for %d: %v", season, err) } log.Printf("Advanced Playoffs import for season: %d", season) time.Sleep(1100 * time.Millisecond) - utils.SleepWithJitter(1250 * time.Millisecond) + utils.SleepWithJitter(1500 * time.Millisecond) } } // importPlayerTotalsScrape fetches & stores scraped regular-season total stats func importPlayerTotalsScrape(db *gorm.DB) { - for season := 2025; season <= 2025; season++ { + for season := 2024; season <= 2025; season++ { if err := services.FetchAndStorePlayerTotalScrapedStats(db, season, false); err != nil { log.Printf("scraped totals import failed for %d: %v", season, err) } log.Printf("Player Totals import for season: %d", season) time.Sleep(1100 * time.Millisecond) - utils.SleepWithJitter(1500 * time.Millisecond) + utils.SleepWithJitter(1250 * time.Millisecond) } } // importPlayerPlayoffsScrape fetches & stores scraped playoff total stats func importPlayerTotalsPlayoffsScrape(db *gorm.DB) { - for season := 2025; season <= 2025; season++ { + for season := 2024; season <= 2025; season++ { if err := services.FetchAndStorePlayerTotalScrapedStats(db, season, true); err != nil { log.Printf("scraped playoffs import failed for %d: %v", season, err) } log.Printf("Player Playoffs Totals import for season: %d", season) time.Sleep(1100 * time.Millisecond) - utils.SleepWithJitter(1750 * time.Millisecond) + utils.SleepWithJitter(1700 * time.Millisecond) } } @@ -61,13 +61,13 @@ func importPlayerTotalsPlayoffsScrape(db *gorm.DB) { func importGameSchedules(db *gorm.DB) { // An NBA season typically runs from October to June months := []string{ - // "october", "november", "december", "january", + "october", "november", "december", "january", + "february", "march", "april", "may", "june", + // "february", "march", "april", "may", "june", - "december", "january", - "february", "march", "april", } - for season := 2023; season <= 2024; season++ { + for season := 2018; season <= 2025; season++ { log.Printf("--- Starting Game Schedule Import for Season: %d ---", season) for _, month := range months { // The service will print a warning and skip if a month has no data (e.g. May/June for a season not yet finished) @@ -78,7 +78,7 @@ func importGameSchedules(db *gorm.DB) { log.Printf("Game schedule import for %s, %d complete.", month, season) // Respectful delay between requests time.Sleep(1100 * time.Millisecond) - utils.SleepWithJitter(1000 * time.Millisecond) + utils.SleepWithJitter(1800 * time.Millisecond) } log.Printf("--- Finished Game Schedule Import for Season: %d ---", season) } @@ -90,9 +90,9 @@ func importBoxScores(db *gorm.DB) { // Define the date range for the import. // The format is: time.Date(year, month, day, hour, min, sec, nsec, location) - to := time.Date(2025, time.April, 10, 0, 0, 0, 0, time.UTC) - // to := time.Now() - from := to.AddDate(0, -1, 0) // 0 years, -3 months, 0 days + // to := time.Date(2019, time.June, 14, 0, 0, 0, 0, time.UTC) + to := time.Now() + from := to.AddDate(-1, 0, 0) // 0 years, -3 months, 0 days log.Printf("--- Starting Box Score Data Import from %s to %s ---", from.Format("2006-01-02"), to.Format("2006-01-02")) diff --git a/services/box_score_scrape_service.go b/services/box_score_scrape_service.go index fb3d516..5098263 100644 --- a/services/box_score_scrape_service.go +++ b/services/box_score_scrape_service.go @@ -49,7 +49,7 @@ func FetchAndStoreBoxScoreDataForDateRange(db *gorm.DB, from, to time.Time) erro log.Printf("Error processing box score for game %s: %v", game.GameID, err) } // Be a good internet citizen and pause between requests. - utils.SleepWithJitter(3000 * time.Millisecond) + utils.SleepWithJitter(2300 * time.Millisecond) } return nil }