├── .prettierrc ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ ├── maven-wrapper.properties │ └── MavenWrapperDownloader.java ├── src ├── main │ ├── java │ │ └── fr │ │ │ └── hippo │ │ │ └── nbastats │ │ │ ├── domain │ │ │ ├── GameNotifier.java │ │ │ ├── Games.java │ │ │ ├── Fouls.java │ │ │ ├── Evaluation.java │ │ │ ├── Stat.java │ │ │ ├── StatTuple.java │ │ │ ├── Identity.java │ │ │ ├── StatFilter.java │ │ │ ├── GameStat.java │ │ │ ├── TeamName.java │ │ │ ├── TeamStat.java │ │ │ └── PlayerStat.java │ │ │ ├── infrastructure │ │ │ └── secondary │ │ │ │ ├── api │ │ │ │ ├── ApiBoxscore.java │ │ │ │ ├── ApiBoxscoreTeam.java │ │ │ │ ├── ApiCard.java │ │ │ │ ├── ApiBoxscoreGame.java │ │ │ │ ├── ApiModule.java │ │ │ │ ├── ApiScoreboardTeam.java │ │ │ │ ├── ApiScoreboard.java │ │ │ │ ├── ApiScoreboardGame.java │ │ │ │ ├── ReleasedGames.java │ │ │ │ ├── ApiNbaGameConverter.java │ │ │ │ ├── ApiScoreboards.java │ │ │ │ ├── ApiBoxscorePlayerStatistics.java │ │ │ │ ├── ApiBoxscorePlayer.java │ │ │ │ ├── NbaTeamIds.java │ │ │ │ └── ApiNbaGames.java │ │ │ │ └── telegram │ │ │ │ └── TelegramGameNotifier.java │ │ │ ├── config │ │ │ ├── RestTemplateConfiguration.java │ │ │ └── StatFilterProperties.java │ │ │ ├── NbaStatsApplication.java │ │ │ └── application │ │ │ └── NbaStatsApplicationService.java │ └── resources │ │ └── application.yml └── test │ ├── java │ └── fr │ │ └── hippo │ │ └── nbastats │ │ ├── NbaStatsApplicationTests.java │ │ ├── config │ │ ├── RestTemplateConfigurationUnitTest.java │ │ └── StatFilterPropertiesUnitTest.java │ │ ├── domain │ │ ├── FoulsUnitTest.java │ │ ├── EvaluationUnitTest.java │ │ ├── StatUnitTest.java │ │ ├── StatTupleUnitTest.java │ │ ├── TeamNameUnitTest.java │ │ ├── IdentityUnitTest.java │ │ ├── GameStatUnitTest.java │ │ ├── StatFilterUnitTest.java │ │ ├── TeamStatUnitTest.java │ │ └── PlayerStatUnitTest.java │ │ ├── infrastructure │ │ └── secondary │ │ │ ├── api │ │ │ ├── NbaTeamIdsUnitTest.java │ │ │ ├── ApiScoreboardsIntTest.java │ │ │ ├── ApiScoreboardUnitTest.java │ │ │ ├── ReleasedGamesUnitTest.java │ │ │ ├── ApiBoxscoreUnitTest.java │ │ │ ├── PlayersExtractorTest.java │ │ │ ├── ApiNbaGamesUnitTest.java │ │ │ └── ApiNbaGameConverterUnitTest.java │ │ │ └── telegram │ │ │ └── TelegramGameNotifierUnitTest.java │ │ ├── application │ │ ├── NbaStatsApplicationServiceIntTest.java │ │ └── NbaStatsApplicationServiceUnitTest.java │ │ └── JsonHelper.java │ └── resources │ └── fixtures │ └── api-boxscore.json ├── .editorconfig ├── .gitignore ├── package.json ├── README.md ├── LICENSE ├── .github └── workflows │ ├── .github-ci.yml │ └── codeql-analysis.yml ├── pom.xml ├── mvnw.cmd └── mvnw /.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 140 2 | singleQuote: true 3 | tabWidth: 4 4 | useTabs: false 5 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hdurix/nba-stats/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/domain/GameNotifier.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | public interface GameNotifier { 4 | void send(GameStat gameStat); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/domain/Games.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import java.util.Optional; 4 | 5 | public interface Games { 6 | Optional findUnreleased(); 7 | } 8 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.2/apache-maven-3.6.2-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar 3 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiBoxscore.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | class ApiBoxscore { 4 | 5 | private ApiBoxscoreGame game; 6 | 7 | public ApiBoxscoreGame getGame() { 8 | return game; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/NbaStatsApplicationTests.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class NbaStatsApplicationTests { 8 | 9 | @Test 10 | void contextLoads() {} 11 | } 12 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | logging.level.org.apache.http.client.protocol.ResponseProcessCookies: ERROR 2 | 3 | games: 4 | storage: target/releases.txt 5 | 6 | # Uncomment to filter stats 7 | #filter: 8 | # high_eval: 30 9 | # player_ids: 10 | # - 2546 11 | # - 203918 12 | # - 203081 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiBoxscoreTeam.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import java.util.Collection; 4 | 5 | class ApiBoxscoreTeam { 6 | 7 | private Collection players; 8 | 9 | public Collection getPlayers() { 10 | return players; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiCard.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | class ApiCard { 4 | 5 | private ApiScoreboardGame cardData; 6 | 7 | public ApiScoreboardGame getCardData() { 8 | return cardData; 9 | } 10 | 11 | public void setCardData(ApiScoreboardGame cardData) { 12 | this.cardData = cardData; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/config/RestTemplateConfigurationUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.config; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class RestTemplateConfigurationUnitTest { 8 | 9 | @Test 10 | void shouldGetRestTemplate() { 11 | assertThat(new RestTemplateConfiguration().restTemplate()).isNotNull(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiBoxscoreGame.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | class ApiBoxscoreGame { 4 | 5 | private ApiBoxscoreTeam homeTeam; 6 | private ApiBoxscoreTeam awayTeam; 7 | 8 | public ApiBoxscoreTeam getHomeTeam() { 9 | return homeTeam; 10 | } 11 | 12 | public ApiBoxscoreTeam getAwayTeam() { 13 | return awayTeam; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiModule.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import java.util.Collection; 4 | 5 | class ApiModule { 6 | 7 | private Collection cards; 8 | 9 | public Collection getCards() { 10 | return cards; 11 | } 12 | 13 | public void setCards(Collection cards) { 14 | this.cards = cards; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/config/RestTemplateConfiguration.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.client.RestTemplate; 6 | 7 | @Configuration 8 | class RestTemplateConfiguration { 9 | 10 | @Bean 11 | RestTemplate restTemplate() { 12 | return new RestTemplate(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/domain/Fouls.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | public class Fouls { 4 | 5 | public static final int MAX_FOULS = 6; 6 | private final int value; 7 | 8 | public Fouls(int value) { 9 | this.value = value; 10 | } 11 | 12 | @Override 13 | public String toString() { 14 | if (value == MAX_FOULS) { 15 | return "*"; 16 | } 17 | 18 | return " "; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/domain/FoulsUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class FoulsUnitTest { 8 | 9 | @Test 10 | void shouldHaveEmptyToStringForLowFouls() { 11 | assertThat(new Fouls(2)).hasToString(" "); 12 | } 13 | 14 | @Test 15 | void shouldHaveMarkedToStringForMaxFouls() { 16 | assertThat(new Fouls(6)).hasToString("*"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/infrastructure/secondary/api/NbaTeamIdsUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import fr.hippo.nbastats.domain.TeamName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class NbaTeamIdsUnitTest { 9 | 10 | @Test 11 | void shouldGetTeamNameFromWrapperId() { 12 | TeamName teamName = NbaTeamIds.findById("1610612743"); 13 | 14 | assertThat(teamName).isEqualTo(TeamName.DENVER); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/application/NbaStatsApplicationServiceIntTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.application; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @SpringBootTest 8 | class NbaStatsApplicationServiceIntTest { 9 | 10 | @Autowired 11 | private NbaStatsApplicationService service; 12 | 13 | @Test 14 | void shouldMakeFullRound() { 15 | service.notifyNextGame(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiScoreboardTeam.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | class ApiScoreboardTeam { 4 | 5 | private String teamId; 6 | private int score; 7 | private int wins; 8 | private int losses; 9 | 10 | public String getTeamId() { 11 | return teamId; 12 | } 13 | 14 | public int getScore() { 15 | return score; 16 | } 17 | 18 | public int getWins() { 19 | return wins; 20 | } 21 | 22 | public int getLosses() { 23 | return losses; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/domain/Evaluation.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | public class Evaluation { 6 | 7 | private final int value; 8 | 9 | public Evaluation(int value) { 10 | this.value = value; 11 | } 12 | 13 | int value() { 14 | return value; 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | if (value >= 0 && value < 10) { 20 | return StringUtils.leftPad(String.valueOf(value), 2); 21 | } 22 | 23 | return String.valueOf(value); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiScoreboard.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import java.util.Collection; 4 | import java.util.stream.Collectors; 5 | 6 | class ApiScoreboard { 7 | 8 | private Collection modules; 9 | 10 | public void setModules(Collection modules) { 11 | this.modules = modules; 12 | } 13 | 14 | public Collection getGames() { 15 | return modules.stream().flatMap(module -> module.getCards().stream()).map(ApiCard::getCardData).collect(Collectors.toList()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ###################### 17 | # Node 18 | ###################### 19 | /node/ 20 | node_tmp/ 21 | node_modules/ 22 | npm-debug.log.* 23 | /.awcache/* 24 | /.cache-loader/* 25 | 26 | ### IntelliJ IDEA ### 27 | .idea 28 | .run 29 | *.iws 30 | *.iml 31 | *.ipr 32 | 33 | ### NetBeans ### 34 | /nbproject/private/ 35 | /nbbuild/ 36 | /dist/ 37 | /nbdist/ 38 | /.nb-gradle/ 39 | build/ 40 | 41 | ### VS Code ### 42 | .vscode/ 43 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/domain/Stat.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | public class Stat { 6 | 7 | private final int value; 8 | 9 | public Stat(int value) { 10 | if (value < 0) { 11 | throw new IllegalArgumentException("stat should be positive"); 12 | } 13 | 14 | this.value = value; 15 | } 16 | 17 | int value() { 18 | return value; 19 | } 20 | 21 | @Override 22 | public String toString() { 23 | if (value < 10) { 24 | return StringUtils.leftPad(String.valueOf(value), 2); 25 | } 26 | 27 | return String.valueOf(value); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/config/StatFilterPropertiesUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.config; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import fr.hippo.nbastats.domain.StatFilter; 6 | import java.util.List; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class StatFilterPropertiesUnitTest { 10 | 11 | @Test 12 | void shouldGetStatFilter() { 13 | StatFilterProperties properties = new StatFilterProperties(); 14 | properties.setHighEval(42); 15 | properties.setPlayerIds(List.of(43)); 16 | StatFilter statFilter = properties.statFilter(); 17 | 18 | assertThat(statFilter.matches(42, 1)).isTrue(); 19 | assertThat(statFilter.matches(1, 43)).isTrue(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/config/StatFilterProperties.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.config; 2 | 3 | import fr.hippo.nbastats.domain.StatFilter; 4 | import java.util.List; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | 7 | @ConfigurationProperties(prefix = "filter") 8 | public class StatFilterProperties { 9 | 10 | private Integer highEval; 11 | private List playerIds; 12 | 13 | void setHighEval(Integer highEval) { 14 | this.highEval = highEval; 15 | } 16 | 17 | void setPlayerIds(List playerIds) { 18 | this.playerIds = playerIds; 19 | } 20 | 21 | public StatFilter statFilter() { 22 | return new StatFilter(highEval, playerIds); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/NbaStatsApplication.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats; 2 | 3 | import fr.hippo.nbastats.config.StatFilterProperties; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 7 | import org.springframework.cache.annotation.EnableCaching; 8 | import org.springframework.scheduling.annotation.EnableScheduling; 9 | 10 | @EnableCaching 11 | @EnableScheduling 12 | @SpringBootApplication 13 | @EnableConfigurationProperties(StatFilterProperties.class) 14 | public class NbaStatsApplication { 15 | 16 | public static void main(String[] args) { 17 | SpringApplication.run(NbaStatsApplication.class, args); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/domain/EvaluationUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class EvaluationUnitTest { 8 | 9 | @Test 10 | void shouldGetValue() { 11 | assertThat(new Evaluation(42).value()).isEqualTo(42); 12 | } 13 | 14 | @Test 15 | void shouldHaveTwoDigitToStringWithoutSpace() { 16 | assertThat(new Evaluation(42)).hasToString("42"); 17 | } 18 | 19 | @Test 20 | void shouldHaveNegativeTwoDigitToStringWithoutSpace() { 21 | assertThat(new Evaluation(-1)).hasToString("-1"); 22 | } 23 | 24 | @Test 25 | void shouldHaveOneDigitToStringWithSpaceWithSpace() { 26 | assertThat(new Evaluation(8)).hasToString(" 8"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiScoreboardGame.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | class ApiScoreboardGame { 4 | 5 | private String gameId; 6 | private int gameStatus; 7 | private ApiScoreboardTeam homeTeam; 8 | private ApiScoreboardTeam awayTeam; 9 | 10 | public String getGameId() { 11 | return gameId; 12 | } 13 | 14 | public void setGameId(String gameId) { 15 | this.gameId = gameId; 16 | } 17 | 18 | public int getGameStatus() { 19 | return gameStatus; 20 | } 21 | 22 | public void setGameStatus(int gameStatus) { 23 | this.gameStatus = gameStatus; 24 | } 25 | 26 | public ApiScoreboardTeam getHomeTeam() { 27 | return homeTeam; 28 | } 29 | 30 | public ApiScoreboardTeam getAwayTeam() { 31 | return awayTeam; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiScoreboardsIntTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import java.time.LocalDate; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | 10 | @SpringBootTest(properties = { "telegram.bot_id=dummy", "telegram.chat_id=dummy" }) 11 | class ApiScoreboardsIntTest { 12 | 13 | @Autowired 14 | private ApiScoreboards scoreboards; 15 | 16 | @Test 17 | void shouldGetScoreboards() { 18 | assertThat(scoreboards.forDate(LocalDate.now())).isNotNull(); 19 | } 20 | 21 | @Test 22 | void shouldGetBoxscore() { 23 | assertThat(scoreboards.boxscoreForGame("0022200003")).isNotNull(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/domain/StatUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class StatUnitTest { 8 | 9 | @Test 10 | void shouldNotBuildWithNegativeValue() { 11 | assertThatThrownBy(() -> new Stat(-1)) 12 | .isExactlyInstanceOf(IllegalArgumentException.class) 13 | .hasMessageContaining("stat should be positive"); 14 | } 15 | 16 | @Test 17 | void shouldGetValue() { 18 | assertThat(new Stat(42).value()).isEqualTo(42); 19 | } 20 | 21 | @Test 22 | void shouldHaveTwoDigitToStringWithoutSpace() { 23 | assertThat(new Stat(42)).hasToString("42"); 24 | } 25 | 26 | @Test 27 | void shouldHaveOneDigitToStringWithSpaceWithSpace() { 28 | assertThat(new Stat(8)).hasToString(" 8"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/domain/StatTuple.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | public class StatTuple { 6 | 7 | private final int success; 8 | private final int total; 9 | 10 | public StatTuple(int success, int total) { 11 | this.success = success; 12 | this.total = total; 13 | } 14 | 15 | int getSuccess() { 16 | return success; 17 | } 18 | 19 | int getTotal() { 20 | return total; 21 | } 22 | 23 | int getMissed() { 24 | return total - success; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | String formattedSuccess = StringUtils.leftPad(String.valueOf(success), 2); 30 | String formattedTotal = StringUtils.rightPad(String.valueOf(total), 2); 31 | 32 | return formattedSuccess + "/" + formattedTotal; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nba-stats", 3 | "version": "0.0.0", 4 | "description": "", 5 | "private": true, 6 | "license": "UNLICENSED", 7 | "cacheDirectories": [ 8 | "node_modules" 9 | ], 10 | "devDependencies": { 11 | "husky": "^4.3.6", 12 | "lint-staged": "^10.5.3", 13 | "prettier-plugin-java": "^1.0.1" 14 | }, 15 | "engines": { 16 | "node": ">=8.9.0" 17 | }, 18 | "husky": { 19 | "hooks": { 20 | "pre-commit": "lint-staged" 21 | } 22 | }, 23 | "lint-staged": { 24 | "{,src/**/}*.java": [ 25 | "prettier --write" 26 | ] 27 | }, 28 | "scripts": { 29 | "prettier:java": "prettier --write \"{,src/**/}*.java\"", 30 | "prettier:java:watch": "onchange '**/*.java' -- prettier --write {{changed}}", 31 | "prettier:java:check": "prettier --check \"{,src/**/}*.java\"" 32 | }, 33 | "dependencies": {} 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram NBA stats bot 2 | 3 | Send NBA games statistics to a [Telegram][] channel (using a [Telegram Bot]) as soon as a game is finished. 4 | 5 | Two running channels are available: 6 | - full version with every stats → https://t.me/nba_games 7 | - lite version with top stats → https://t.me/nba_top_stats 8 | 9 | ## Technical environment 10 | 11 | * Java 11 12 | * Spring Boot 13 | * Maven 14 | 15 | [![codecov](https://codecov.io/gh/hdurix/nba-stats/branch/master/graph/badge.svg)](https://codecov.io/gh/hdurix/nba-stats) 16 | 17 | ## Methodologies 18 | 19 | * TDD 20 | * DDD 21 | * Hexagonal architecture 22 | 23 | ## Environment variables 24 | 25 | Before running the app (development, tests, production), be sure to provide 2 environment variables: 26 | - `telegram.bot_id` 27 | - `telegram.chat_id` 28 | 29 | For instance with: 30 | 31 | > TELEGRAM_BOT_ID=123:abc TELEGRAM_CHAT_ID=123 ./mvnw clean package 32 | 33 | [Telegram]: https://telegram.org/ 34 | [Telegram Bot]: https://core.telegram.org/bots 35 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/domain/StatTupleUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class StatTupleUnitTest { 8 | 9 | @Test 10 | void shouldGetValues() { 11 | StatTuple statTuple = new StatTuple(2, 12); 12 | 13 | assertThat(statTuple.getSuccess()).isEqualTo(2); 14 | assertThat(statTuple.getTotal()).isEqualTo(12); 15 | assertThat(statTuple.getMissed()).isEqualTo(10); 16 | } 17 | 18 | @Test 19 | void shouldToStringWithTwoLowValuesHaveTwoSpaces() { 20 | assertThat(new StatTuple(2, 2)).hasToString(" 2/2 "); 21 | } 22 | 23 | @Test 24 | void shouldToStringWithHighTotalHaveOneSpace() { 25 | assertThat(new StatTuple(2, 12)).hasToString(" 2/12"); 26 | } 27 | 28 | @Test 29 | void shouldToStringWithHighSuccessHaveNoSpace() { 30 | assertThat(new StatTuple(11, 12)).hasToString("11/12"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/domain/TeamNameUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import java.util.Arrays; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class TeamNameUnitTest { 9 | 10 | @Test 11 | void shouldGetCity() { 12 | assertThat(TeamName.ORLANDO.city()).isEqualTo("Orlando"); 13 | } 14 | 15 | @Test 16 | void shouldGetNickname() { 17 | assertThat(TeamName.ORLANDO.nickname()).isEqualTo("Magic"); 18 | } 19 | 20 | @Test 21 | void shouldGetFullName() { 22 | assertThat(TeamName.ORLANDO.fullName()).isEqualTo("Orlando Magic"); 23 | } 24 | 25 | @Test 26 | void shouldHaveToStringCenteredWithHyphens() { 27 | assertThat(TeamName.ORLANDO.toString()).isEqualTo("------- Orlando Magic -------"); 28 | assertThat(TeamName.MINNESOTA.toString()).isEqualTo("-- Minnesota Timberwolves ---"); 29 | 30 | Arrays.stream(TeamName.values()).map(TeamName::toString).forEach(System.out::println); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Hippolyte Durix 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/application/NbaStatsApplicationService.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.application; 2 | 3 | import fr.hippo.nbastats.domain.GameNotifier; 4 | import fr.hippo.nbastats.domain.Games; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.scheduling.annotation.Scheduled; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Service 11 | class NbaStatsApplicationService { 12 | 13 | private static final int ONE_SECOND = 1000; 14 | private static final int ONE_MINUTE = 60 * ONE_SECOND; 15 | 16 | private static final Logger log = LoggerFactory.getLogger(NbaStatsApplicationService.class); 17 | 18 | private final Games games; 19 | private final GameNotifier notifier; 20 | 21 | NbaStatsApplicationService(Games games, GameNotifier notifier) { 22 | this.games = games; 23 | this.notifier = notifier; 24 | } 25 | 26 | @Scheduled(initialDelay = ONE_SECOND, fixedDelay = ONE_MINUTE) 27 | void notifyNextGame() { 28 | log.debug("Check if a new game has to be notified"); 29 | 30 | games.findUnreleased().ifPresent(notifier::send); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/domain/IdentityUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class IdentityUnitTest { 8 | 9 | @Test 10 | void shouldGetId() { 11 | assertThat(defaultIdentity().getId()).isEqualTo(42); 12 | } 13 | 14 | @Test 15 | void shouldHaveEmptyIdentity() { 16 | assertThat(new Identity(42)).hasToString("?. ??? "); 17 | } 18 | 19 | @Test 20 | void shouldHaveTruncatedLongName() { 21 | assertThat(new Identity(42, "Giannis", "Antetokounmpo")).hasToString("G. Antetoko"); 22 | } 23 | 24 | @Test 25 | void shouldHaveFullName() { 26 | assertThat(new Identity(42, "Brook", "Lopez")).hasToString("B. Lopez "); 27 | } 28 | 29 | @Test 30 | void shouldBeUnknown() { 31 | assertThat(unknownIdentity().isUnknown()).isTrue(); 32 | } 33 | 34 | public static Identity unknownIdentity() { 35 | return new Identity(42); 36 | } 37 | 38 | public static Identity defaultIdentity() { 39 | return new Identity(42, "Brook", "Lopez"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/domain/GameStatUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import static fr.hippo.nbastats.domain.TeamStatUnitTest.*; 4 | import static org.assertj.core.api.Assertions.*; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class GameStatUnitTest { 9 | 10 | @Test 11 | void shouldNotBuildWithoutAwayTeam() { 12 | assertThatThrownBy(() -> new GameStat(null, detroit())) 13 | .isExactlyInstanceOf(IllegalArgumentException.class) 14 | .hasMessageContaining("away"); 15 | } 16 | 17 | @Test 18 | void shouldNotBuildWithoutHomeTeam() { 19 | assertThatThrownBy(() -> new GameStat(indiana(), null)) 20 | .isExactlyInstanceOf(IllegalArgumentException.class) 21 | .hasMessageContaining("home"); 22 | } 23 | 24 | @Test 25 | void shouldHaveFullToString() { 26 | assertThat(defaultGameStat()) 27 | .hasToString(" Pacers 123 - Pistons 124 \n" + " 8-18 12-13 \n\n" + indiana() + "\n\n" + detroit()); 28 | } 29 | 30 | public static GameStat defaultGameStat() { 31 | return new GameStat(indiana(), detroit()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/.github-ci.yml: -------------------------------------------------------------------------------- 1 | name: Application CI 2 | on: push 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Set up JDK 1.11 9 | uses: actions/setup-java@v1 10 | with: 11 | java-version: '11.x' 12 | - name: Test with Maven 13 | env: 14 | TELEGRAM_BOT_ID: ${{ secrets.TELEGRAM_BOT_ID }} 15 | TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} 16 | run: | 17 | chmod +x mvnw 18 | ./mvnw -B test 19 | - name: Upload coverage to Codecov 20 | uses: codecov/codecov-action@v1 21 | with: 22 | token: ${{ secrets.CODECOV_TOKEN }} 23 | prettier: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Set up Node 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: '10.x' 31 | - run: npm install 32 | - name: Check prettier 33 | run: npm run prettier:java:check 34 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/domain/Identity.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import java.util.Optional; 4 | import org.apache.commons.lang3.StringUtils; 5 | 6 | public class Identity { 7 | 8 | public static final int MAX_LENGTH = 11; 9 | private final int id; 10 | private final Optional firstName; 11 | private final Optional lastName; 12 | 13 | public Identity(int id, String firstName, String lastName) { 14 | this.id = id; 15 | this.firstName = Optional.ofNullable(firstName); 16 | this.lastName = Optional.ofNullable(lastName); 17 | } 18 | 19 | public Identity(int id) { 20 | this(id, null, null); 21 | } 22 | 23 | public int getId() { 24 | return id; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return truncateOrFill(firstName.orElse("?").charAt(0) + ". " + lastName.orElse("???")); 30 | } 31 | 32 | private String truncateOrFill(String name) { 33 | if (name.length() > MAX_LENGTH) { 34 | return name.substring(0, MAX_LENGTH); 35 | } 36 | 37 | return StringUtils.rightPad(name, MAX_LENGTH); 38 | } 39 | 40 | public boolean isUnknown() { 41 | return firstName.isEmpty(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/application/NbaStatsApplicationServiceUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.application; 2 | 3 | import static org.mockito.Mockito.*; 4 | 5 | import fr.hippo.nbastats.domain.GameNotifier; 6 | import fr.hippo.nbastats.domain.GameStat; 7 | import fr.hippo.nbastats.domain.GameStatUnitTest; 8 | import fr.hippo.nbastats.domain.Games; 9 | import java.util.Optional; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.mockito.InjectMocks; 13 | import org.mockito.Mock; 14 | import org.springframework.test.context.junit.jupiter.SpringExtension; 15 | 16 | @ExtendWith(SpringExtension.class) 17 | class NbaStatsApplicationServiceUnitTest { 18 | 19 | @Mock 20 | private Games games; 21 | 22 | @Mock 23 | private GameNotifier notifier; 24 | 25 | @InjectMocks 26 | private NbaStatsApplicationService service; 27 | 28 | @Test 29 | void shouldNotNotifyIfNoGame() { 30 | service.notifyNextGame(); 31 | 32 | verify(notifier, never()).send(any()); 33 | } 34 | 35 | @Test 36 | void shouldNotifyGame() { 37 | GameStat gameStat = GameStatUnitTest.defaultGameStat(); 38 | when(games.findUnreleased()).thenReturn(Optional.of(gameStat)); 39 | 40 | service.notifyNextGame(); 41 | 42 | verify(notifier).send(gameStat); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/domain/StatFilter.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import java.util.List; 4 | 5 | public class StatFilter { 6 | 7 | private final Integer highEval; 8 | private final List playerIds; 9 | 10 | public StatFilter(Integer highEval, List playerIds) { 11 | this.highEval = highEval; 12 | this.playerIds = playerIds; 13 | } 14 | 15 | public boolean matches(int eval, int playerId) { 16 | if (noHighEval() && noWantedPlayers()) { 17 | return true; 18 | } 19 | if (hasHighEval() && hasWantedPlayers()) { 20 | return isHigh(eval) || isWanted(playerId); 21 | } 22 | if (hasHighEval()) { 23 | return isHigh(eval); 24 | } 25 | return isWanted(playerId); 26 | } 27 | 28 | private boolean noHighEval() { 29 | return !hasHighEval(); 30 | } 31 | 32 | private boolean hasHighEval() { 33 | return highEval != null; 34 | } 35 | 36 | private boolean noWantedPlayers() { 37 | return !hasWantedPlayers(); 38 | } 39 | 40 | private boolean hasWantedPlayers() { 41 | return playerIds != null; 42 | } 43 | 44 | private boolean isHigh(int eval) { 45 | return eval >= highEval; 46 | } 47 | 48 | private boolean isWanted(int playerId) { 49 | return playerIds.contains(playerId); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/domain/GameStat.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.springframework.util.Assert; 5 | 6 | public class GameStat { 7 | 8 | private static final int LINE_SIZE = 29; 9 | 10 | private final TeamStat away; 11 | private final TeamStat home; 12 | 13 | public GameStat(TeamStat away, TeamStat home) { 14 | Assert.notNull(away, "missing away team"); 15 | Assert.notNull(home, "missing home team"); 16 | 17 | this.away = away; 18 | this.home = home; 19 | } 20 | 21 | @Override 22 | public String toString() { 23 | String scoreLine = StringUtils.center(score(), LINE_SIZE); 24 | return scoreLine + "\n" + winsLossesLine(scoreLine) + "\n\n" + away + "\n\n" + home; 25 | } 26 | 27 | private String score() { 28 | return away.getName().nickname() + " " + away.getScore() + " - " + home.getName().nickname() + " " + home.getScore(); 29 | } 30 | 31 | private String winsLossesLine(String scoreLine) { 32 | int indexOfScoreCentralDash = scoreLine.indexOf('-'); 33 | String nineSpaces = " ".repeat(9); 34 | 35 | String winLosses = away.getWins() + "-" + away.getLosses() + nineSpaces + home.getWins() + "-" + home.getLosses(); 36 | 37 | int indexOfWinsLossesCentralSpace = winLosses.indexOf(nineSpaces) + 4; 38 | 39 | String leftSpaces = " ".repeat(indexOfScoreCentralDash - indexOfWinsLossesCentralSpace); 40 | return StringUtils.rightPad(leftSpaces + winLosses, LINE_SIZE); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/infrastructure/secondary/telegram/TelegramGameNotifierUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.telegram; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | import static org.mockito.Mockito.*; 5 | 6 | import fr.hippo.nbastats.domain.GameStatUnitTest; 7 | import java.net.URI; 8 | import java.net.URISyntaxException; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.mockito.ArgumentCaptor; 13 | import org.mockito.Mock; 14 | import org.springframework.test.context.junit.jupiter.SpringExtension; 15 | import org.springframework.web.client.RestTemplate; 16 | 17 | @ExtendWith(SpringExtension.class) 18 | class TelegramGameNotifierUnitTest { 19 | 20 | @Mock 21 | private RestTemplate restTemplate; 22 | 23 | private TelegramGameNotifier notifier; 24 | 25 | @BeforeEach 26 | void setup() { 27 | notifier = new TelegramGameNotifier(restTemplate, "123:abc", "456"); 28 | } 29 | 30 | @Test 31 | void shouldSendToTelegramBot() throws URISyntaxException { 32 | notifier.send(GameStatUnitTest.defaultGameStat()); 33 | 34 | ArgumentCaptor captor = ArgumentCaptor.forClass(URI.class); 35 | verify(restTemplate).getForEntity(captor.capture(), eq(Object.class)); 36 | 37 | URI url = captor.getValue(); 38 | assertThat(url.toString()) 39 | .startsWith("https://api.telegram.org/bot123:abc/sendMessage?chat_id=456&parse_mode=html&text=%3Cpre%3E%20%20Pacers"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiScoreboardUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import fr.hippo.nbastats.JsonHelper; 6 | import java.io.IOException; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import org.junit.jupiter.api.Test; 10 | 11 | class ApiScoreboardUnitTest { 12 | 13 | @Test 14 | void shouldReadFromJson() { 15 | ApiScoreboard scoreboard = JsonHelper.readFromJson(defaultJson(), ApiScoreboard.class); 16 | 17 | ApiScoreboardGame game = scoreboard.getGames().iterator().next(); 18 | assertThat(game.getGameId()).isEqualTo("0022200003"); 19 | assertThat(game.getGameStatus()).isEqualTo(3); 20 | assertThat(game.getHomeTeam().getTeamId()).isEqualTo("1610612765"); 21 | assertThat(game.getHomeTeam().getScore()).isEqualTo(113); 22 | assertThat(game.getHomeTeam().getWins()).isEqualTo(1); 23 | assertThat(game.getHomeTeam().getLosses()).isEqualTo(0); 24 | assertThat(game.getAwayTeam().getTeamId()).isEqualTo("1610612753"); 25 | assertThat(game.getAwayTeam().getScore()).isEqualTo(109); 26 | assertThat(game.getAwayTeam().getWins()).isEqualTo(0); 27 | assertThat(game.getAwayTeam().getLosses()).isEqualTo(1); 28 | } 29 | 30 | private String defaultJson() { 31 | try { 32 | return Files.readString(Path.of("src/test/resources/fixtures/api-scoreboard.json")); 33 | } catch (IOException e) { 34 | throw new RuntimeException(e); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/infrastructure/secondary/api/ReleasedGames.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.nio.file.Paths; 7 | import java.nio.file.StandardOpenOption; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.Stream; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Value; 15 | import org.springframework.stereotype.Repository; 16 | 17 | @Repository 18 | class ReleasedGames { 19 | 20 | private static final Logger log = LoggerFactory.getLogger(ReleasedGames.class); 21 | 22 | private static final String ID_SEPARATOR = "\n"; 23 | 24 | private final Path storagePath; 25 | 26 | ReleasedGames(@Value("${games.storage}") String storageUrl) { 27 | this.storagePath = Paths.get(storageUrl); 28 | } 29 | 30 | void add(String gameId) { 31 | String toStore = gameId + ID_SEPARATOR; 32 | try { 33 | Files.createDirectories(storagePath.getParent()); 34 | Files.write(storagePath, toStore.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND); 35 | } catch (IOException e) { 36 | log.error("Cannot add game to storage", e); 37 | } 38 | } 39 | 40 | List findAll() { 41 | try { 42 | return Stream.of(new String(Files.readAllBytes(storagePath)).split("\n")).collect(Collectors.toList()); 43 | } catch (IOException e) { 44 | return new ArrayList<>(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiNbaGameConverter.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import fr.hippo.nbastats.config.StatFilterProperties; 4 | import fr.hippo.nbastats.domain.GameStat; 5 | import fr.hippo.nbastats.domain.PlayerStat; 6 | import fr.hippo.nbastats.domain.TeamStat; 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | class ApiNbaGameConverter { 13 | 14 | private final StatFilterProperties statFilterProperties; 15 | 16 | ApiNbaGameConverter(StatFilterProperties statFilterProperties) { 17 | this.statFilterProperties = statFilterProperties; 18 | } 19 | 20 | GameStat toDomain(ApiScoreboardGame gameDetails, ApiBoxscoreGame boxscore) { 21 | TeamStat away = toTeamStat(boxscore.getAwayTeam(), gameDetails.getAwayTeam()); 22 | TeamStat home = toTeamStat(boxscore.getHomeTeam(), gameDetails.getHomeTeam()); 23 | return new GameStat(away, home); 24 | } 25 | 26 | private TeamStat toTeamStat(ApiBoxscoreTeam boxscore, ApiScoreboardTeam teamScore) { 27 | return TeamStat 28 | .builder() 29 | .filter(statFilterProperties.statFilter()) 30 | .name(NbaTeamIds.findById(teamScore.getTeamId())) 31 | .score(teamScore.getScore()) 32 | .wins(teamScore.getWins()) 33 | .losses(teamScore.getLosses()) 34 | .players(extractStat(boxscore)) 35 | .build(); 36 | } 37 | 38 | static List extractStat(ApiBoxscoreTeam boxscoreTeam) { 39 | return boxscoreTeam.getPlayers().stream().map(ApiBoxscorePlayer::toDomain).collect(Collectors.toList()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/infrastructure/secondary/telegram/TelegramGameNotifier.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.telegram; 2 | 3 | import fr.hippo.nbastats.domain.GameNotifier; 4 | import fr.hippo.nbastats.domain.GameStat; 5 | import java.net.URI; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.client.RestTemplate; 11 | import org.springframework.web.util.UriComponentsBuilder; 12 | 13 | @Component 14 | class TelegramGameNotifier implements GameNotifier { 15 | 16 | private static final Logger log = LoggerFactory.getLogger(TelegramGameNotifier.class); 17 | 18 | private final RestTemplate restTemplate; 19 | private final String chatId; 20 | private final String botId; 21 | 22 | TelegramGameNotifier( 23 | RestTemplate restTemplate, 24 | @Value("${telegram.bot_id}") String botId, 25 | @Value("${telegram.chat_id}") String chatId 26 | ) { 27 | this.restTemplate = restTemplate; 28 | this.chatId = chatId; 29 | this.botId = botId; 30 | } 31 | 32 | @Override 33 | public void send(GameStat gameStat) { 34 | log.info("notify Game: {}", gameStat.toString().split("\n")[0]); 35 | 36 | URI uri = UriComponentsBuilder 37 | .fromUriString("https://api.telegram.org/bot") 38 | .path("{bot_id}/sendMessage") 39 | .queryParam("chat_id", chatId) 40 | .queryParam("parse_mode", "html") 41 | .queryParam("text", "
" + gameStat + "
") 42 | .buildAndExpand(botId) 43 | .toUri(); 44 | 45 | restTemplate.getForEntity(uri, Object.class); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiScoreboards.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import java.time.LocalDate; 4 | import java.time.format.DateTimeFormatter; 5 | import org.springframework.http.HttpEntity; 6 | import org.springframework.http.HttpHeaders; 7 | import org.springframework.http.HttpMethod; 8 | import org.springframework.stereotype.Repository; 9 | import org.springframework.web.client.RestTemplate; 10 | 11 | @Repository 12 | class ApiScoreboards { 13 | 14 | private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("MM/dd/yyyy"); 15 | 16 | private final RestTemplate rest; 17 | 18 | ApiScoreboards(RestTemplate rest) { 19 | this.rest = rest; 20 | } 21 | 22 | ApiScoreboard forDate(LocalDate date) { 23 | String url = "https://core-api.nba.com/cp/api/v1.9/feeds/gamecardfeed?gamedate=" + date.format(DATE_FORMATTER) + "&platform=web"; 24 | 25 | return rest.exchange(url, HttpMethod.GET, entityWithNbaHeaders(), ApiScoreboard.class).getBody(); 26 | } 27 | 28 | public ApiBoxscoreGame boxscoreForGame(String gameId) { 29 | String url = "https://cdn.nba.com/static/json/liveData/boxscore/boxscore_" + gameId + ".json"; 30 | 31 | return rest.exchange(url, HttpMethod.GET, entityWithNbaHeaders(), ApiBoxscore.class).getBody().getGame(); 32 | } 33 | 34 | private static HttpEntity entityWithNbaHeaders() { 35 | HttpHeaders headers = new HttpHeaders(); 36 | headers.set("Origin", "https://www.nba.com/"); 37 | headers.set("Referer", "https://www.nba.com/"); 38 | headers.set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64)"); 39 | headers.set("ocp-apim-subscription-key", "747fa6900c6c4e89a58b81b72f36eb96"); 40 | 41 | return new HttpEntity<>(headers); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/JsonHelper.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.DeserializationFeature; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.SerializationFeature; 8 | import com.fasterxml.jackson.databind.json.JsonMapper; 9 | import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; 10 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 11 | import java.io.IOException; 12 | 13 | public final class JsonHelper { 14 | 15 | private static final ObjectMapper jsonMapper = jsonMapper(); 16 | 17 | private JsonHelper() {} 18 | 19 | public static ObjectMapper jsonMapper() { 20 | return JsonMapper 21 | .builder() 22 | .serializationInclusion(JsonInclude.Include.NON_NULL) 23 | .addModule(new JavaTimeModule()) 24 | .addModules(new Jdk8Module()) 25 | .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) 26 | .disable(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY) 27 | .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 28 | .build(); 29 | } 30 | 31 | public static String writeAsString(T object) { 32 | try { 33 | return jsonMapper.writeValueAsString(object); 34 | } catch (JsonProcessingException e) { 35 | throw new AssertionError("Error serializing object: " + e.getMessage(), e); 36 | } 37 | } 38 | 39 | public static T readFromJson(String json, Class clazz) { 40 | try { 41 | return jsonMapper.readValue(json, clazz); 42 | } catch (IOException e) { 43 | throw new AssertionError("Error reading value from json: " + e.getMessage(), e); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiBoxscorePlayerStatistics.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | class ApiBoxscorePlayerStatistics { 4 | 5 | private int foulsPersonal; 6 | private int points; 7 | private int reboundsTotal; 8 | private int assists; 9 | private int blocks; 10 | private int steals; 11 | private int fieldGoalsMade; 12 | private int fieldGoalsAttempted; 13 | private int threePointersMade; 14 | private int threePointersAttempted; 15 | private int freeThrowsMade; 16 | private int freeThrowsAttempted; 17 | private int turnovers; 18 | private String minutesCalculated; 19 | 20 | public int getFoulsPersonal() { 21 | return foulsPersonal; 22 | } 23 | 24 | public int getPoints() { 25 | return points; 26 | } 27 | 28 | public int getReboundsTotal() { 29 | return reboundsTotal; 30 | } 31 | 32 | public int getAssists() { 33 | return assists; 34 | } 35 | 36 | public int getBlocks() { 37 | return blocks; 38 | } 39 | 40 | public int getSteals() { 41 | return steals; 42 | } 43 | 44 | public int getFieldGoalsMade() { 45 | return fieldGoalsMade; 46 | } 47 | 48 | public int getFieldGoalsAttempted() { 49 | return fieldGoalsAttempted; 50 | } 51 | 52 | public int getThreePointersMade() { 53 | return threePointersMade; 54 | } 55 | 56 | public int getThreePointersAttempted() { 57 | return threePointersAttempted; 58 | } 59 | 60 | public int getFreeThrowsMade() { 61 | return freeThrowsMade; 62 | } 63 | 64 | public int getFreeThrowsAttempted() { 65 | return freeThrowsAttempted; 66 | } 67 | 68 | public int getTurnovers() { 69 | return turnovers; 70 | } 71 | 72 | public String getMinutesCalculated() { 73 | return minutesCalculated; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/infrastructure/secondary/api/ReleasedGamesUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.Files; 7 | import java.nio.file.Paths; 8 | import org.junit.jupiter.api.AfterEach; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.springframework.test.context.junit.jupiter.SpringExtension; 13 | 14 | @ExtendWith(SpringExtension.class) 15 | class ReleasedGamesUnitTest { 16 | 17 | private static final String STORAGE = "target/releases-test.txt"; 18 | 19 | private ReleasedGames releasedGames; 20 | 21 | @BeforeEach 22 | void setup() { 23 | releasedGames = new ReleasedGames(STORAGE); 24 | } 25 | 26 | @AfterEach 27 | void deleteStorage() throws IOException { 28 | Files.deleteIfExists(Paths.get(STORAGE)); 29 | } 30 | 31 | @Test 32 | void shouldAddRelease() throws IOException { 33 | releasedGames.add("1234"); 34 | 35 | assertThat(new String(Files.readAllBytes(Paths.get(STORAGE)))).isEqualTo("1234\n"); 36 | } 37 | 38 | @Test 39 | void shouldNotAddRelease() throws IOException { 40 | String storageUrl = "/root/super-powered"; 41 | 42 | new ReleasedGames(storageUrl).add("1234"); 43 | 44 | assertThat(Files.exists(Paths.get(storageUrl))).isFalse(); 45 | } 46 | 47 | @Test 48 | void shouldFindAllReleases() { 49 | releasedGames.add("1234"); 50 | releasedGames.add("5678"); 51 | 52 | assertThat(releasedGames.findAll()).containsExactly("1234", "5678"); 53 | } 54 | 55 | @Test 56 | void shouldFindEmptyReleaseOnError() throws IOException { 57 | String storageUrl = "/root/super-powered"; 58 | 59 | assertThat(releasedGames.findAll()).isEmpty(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/domain/StatFilterUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import java.util.List; 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class StatFilterUnitTest { 9 | 10 | @Test 11 | void shouldMatchWithOnlyHighEval() { 12 | StatFilter statFilter = new StatFilter(30, null); 13 | 14 | assertThat(statFilter.matches(42, 42)).isTrue(); 15 | } 16 | 17 | @Test 18 | void shouldNotMatchWithOnlyLowEval() { 19 | StatFilter statFilter = new StatFilter(30, null); 20 | 21 | assertThat(statFilter.matches(1, 42)).isFalse(); 22 | } 23 | 24 | @Test 25 | void shouldMatchWithHighEval() { 26 | StatFilter statFilter = new StatFilter(30, List.of(1)); 27 | 28 | assertThat(statFilter.matches(42, 42)).isTrue(); 29 | } 30 | 31 | @Test 32 | void shouldMatchWithOnlyMatchingPlayerId() { 33 | StatFilter statFilter = new StatFilter(null, List.of(1)); 34 | 35 | assertThat(statFilter.matches(42, 1)).isTrue(); 36 | } 37 | 38 | @Test 39 | void shouldNotMatchWithOnlyNotMatchingPlayerId() { 40 | StatFilter statFilter = new StatFilter(null, List.of(1)); 41 | 42 | assertThat(statFilter.matches(42, 42)).isFalse(); 43 | } 44 | 45 | @Test 46 | void shouldMatchWithMatchingPlayerId() { 47 | StatFilter statFilter = new StatFilter(30, List.of(1)); 48 | 49 | assertThat(statFilter.matches(-1, 1)).isTrue(); 50 | } 51 | 52 | @Test 53 | void shouldNotMatchWithNotMatchingFilters() { 54 | StatFilter statFilter = new StatFilter(30, List.of(1)); 55 | 56 | assertThat(statFilter.matches(-1, 42)).isFalse(); 57 | } 58 | 59 | @Test 60 | void shouldMatchWithEmptyFilter() { 61 | StatFilter statFilter = empty(); 62 | 63 | assertThat(statFilter.matches(-1, 1)).isTrue(); 64 | } 65 | 66 | public static StatFilter empty() { 67 | return new StatFilter(null, null); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiBoxscorePlayer.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import fr.hippo.nbastats.domain.Fouls; 4 | import fr.hippo.nbastats.domain.Identity; 5 | import fr.hippo.nbastats.domain.PlayerStat; 6 | import fr.hippo.nbastats.domain.Stat; 7 | import fr.hippo.nbastats.domain.StatTuple; 8 | 9 | class ApiBoxscorePlayer { 10 | 11 | private int personId; 12 | private String firstName; 13 | private String familyName; 14 | private ApiBoxscorePlayerStatistics statistics; 15 | 16 | public int getPersonId() { 17 | return personId; 18 | } 19 | 20 | public String getFirstName() { 21 | return firstName; 22 | } 23 | 24 | public String getFamilyName() { 25 | return familyName; 26 | } 27 | 28 | public ApiBoxscorePlayerStatistics getStatistics() { 29 | return statistics; 30 | } 31 | 32 | PlayerStat toDomain() { 33 | Identity identity = new Identity(personId, firstName, familyName); 34 | 35 | return PlayerStat 36 | .builder() 37 | .identity(identity) 38 | .fouls(new Fouls(statistics.getFoulsPersonal())) 39 | .points(new Stat(statistics.getPoints())) 40 | .rebounds(new Stat(statistics.getReboundsTotal())) 41 | .assists(new Stat(statistics.getAssists())) 42 | .blocks(new Stat(statistics.getBlocks())) 43 | .steals(new Stat(statistics.getSteals())) 44 | .fieldGoals(new StatTuple(statistics.getFieldGoalsMade(), statistics.getFieldGoalsAttempted())) 45 | .threePoints(new StatTuple(statistics.getThreePointersMade(), statistics.getThreePointersAttempted())) 46 | .freeThrows(new StatTuple(statistics.getFreeThrowsMade(), statistics.getFreeThrowsAttempted())) 47 | .turnovers(new Stat(statistics.getTurnovers())) 48 | .minutes(new Stat(parseMinutes(statistics.getMinutesCalculated()))) 49 | .build(); 50 | } 51 | 52 | private static int parseMinutes(String minutes) { 53 | return Integer.parseInt(minutes.split("PT")[1].split("M")[0]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/infrastructure/secondary/api/NbaTeamIds.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import fr.hippo.nbastats.domain.TeamName; 4 | import java.util.AbstractMap.SimpleImmutableEntry; 5 | import java.util.Map; 6 | 7 | class NbaTeamIds { 8 | 9 | private static final Map TEAM_IDS = Map.ofEntries( 10 | entry("1610612737", TeamName.ATLANTA), 11 | entry("1610612738", TeamName.BOSTON), 12 | entry("1610612751", TeamName.BROOKLYN), 13 | entry("1610612766", TeamName.CHARLOTTE), 14 | entry("1610612741", TeamName.CHICAGO), 15 | entry("1610612739", TeamName.CLEVELAND), 16 | entry("1610612742", TeamName.DALLAS), 17 | entry("1610612743", TeamName.DENVER), 18 | entry("1610612765", TeamName.DETROIT), 19 | entry("1610612744", TeamName.GOLDEN_STATE), 20 | entry("1610612745", TeamName.HOUSTON), 21 | entry("1610612754", TeamName.INDIANA), 22 | entry("1610612746", TeamName.CLIPPERS), 23 | entry("1610612747", TeamName.LAKERS), 24 | entry("1610612763", TeamName.MEMPHIS), 25 | entry("1610612748", TeamName.MIAMI), 26 | entry("1610612749", TeamName.MILWAUKEE), 27 | entry("1610612750", TeamName.MINNESOTA), 28 | entry("1610612740", TeamName.NEW_ORLEANS), 29 | entry("1610612752", TeamName.NEW_YORK), 30 | entry("1610612760", TeamName.OKLAHOMA), 31 | entry("1610612753", TeamName.ORLANDO), 32 | entry("1610612755", TeamName.PHILADELPHIA), 33 | entry("1610612756", TeamName.PHOENIX), 34 | entry("1610612757", TeamName.PORTLAND), 35 | entry("1610612758", TeamName.SACRAMENTO), 36 | entry("1610612759", TeamName.SAN_ANTONIO), 37 | entry("1610612761", TeamName.TORONTO), 38 | entry("1610612762", TeamName.UTAH), 39 | entry("1610612764", TeamName.WASHINGTON) 40 | ); 41 | 42 | private static SimpleImmutableEntry entry(String id, TeamName teamName) { 43 | return new SimpleImmutableEntry<>(id, teamName); 44 | } 45 | 46 | static TeamName findById(String wrapperId) { 47 | return TEAM_IDS.get(wrapperId); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiBoxscoreUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import fr.hippo.nbastats.JsonHelper; 6 | import java.io.IOException; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import org.junit.jupiter.api.Test; 10 | 11 | class ApiBoxscoreUnitTest { 12 | 13 | @Test 14 | void shouldReadFromJson() { 15 | ApiBoxscore scoreboard = JsonHelper.readFromJson(defaultJson(), ApiBoxscore.class); 16 | 17 | ApiBoxscoreGame game = scoreboard.getGame(); 18 | 19 | ApiBoxscoreTeam homeTeam = game.getHomeTeam(); 20 | ApiBoxscorePlayer player = homeTeam.getPlayers().iterator().next(); 21 | assertThat(player.getPersonId()).isEqualTo(1630180); 22 | assertThat(player.getFirstName()).isEqualTo("Saddiq"); 23 | assertThat(player.getFamilyName()).isEqualTo("Bey"); 24 | ApiBoxscorePlayerStatistics statistics = player.getStatistics(); 25 | assertThat(statistics.getFoulsPersonal()).isEqualTo(2); 26 | assertThat(statistics.getPoints()).isEqualTo(8); 27 | assertThat(statistics.getReboundsTotal()).isEqualTo(6); 28 | assertThat(statistics.getAssists()).isEqualTo(3); 29 | assertThat(statistics.getBlocks()).isEqualTo(0); 30 | assertThat(statistics.getSteals()).isEqualTo(1); 31 | assertThat(statistics.getFieldGoalsMade()).isEqualTo(2); 32 | assertThat(statistics.getFieldGoalsAttempted()).isEqualTo(5); 33 | assertThat(statistics.getThreePointersMade()).isEqualTo(0); 34 | assertThat(statistics.getThreePointersAttempted()).isEqualTo(2); 35 | assertThat(statistics.getFreeThrowsMade()).isEqualTo(4); 36 | assertThat(statistics.getFreeThrowsAttempted()).isEqualTo(4); 37 | assertThat(statistics.getTurnovers()).isEqualTo(1); 38 | assertThat(statistics.getMinutesCalculated()).isEqualTo("PT31M"); 39 | 40 | assertThat(game.getAwayTeam()).isNotNull(); 41 | } 42 | 43 | private String defaultJson() { 44 | try { 45 | return Files.readString(Path.of("src/test/resources/fixtures/api-boxscore.json")); 46 | } catch (IOException e) { 47 | throw new RuntimeException(e); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/infrastructure/secondary/api/PlayersExtractorTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import com.drmilk.nbawrapper.domain.utils.player.Draft; 4 | import com.drmilk.nbawrapper.domain.utils.player.LeaguePlayer; 5 | import com.drmilk.nbawrapper.domain.utils.player.LeaguePlayersResponse; 6 | import com.drmilk.nbawrapper.utils.QueryManager; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import fr.hippo.nbastats.domain.TeamName; 9 | import java.io.IOException; 10 | import java.util.List; 11 | import java.util.function.Function; 12 | import org.apache.http.HttpResponse; 13 | import org.junit.jupiter.api.Disabled; 14 | import org.junit.jupiter.api.Test; 15 | 16 | @Disabled("no need to be automated") 17 | class PlayersExtractorTest { 18 | 19 | private static final int CURRENT_SEASON = 2025; 20 | private static final String SOURCE_BASE_URL = "http://data.nba.net/data/10s/prod/v1"; 21 | 22 | private final ObjectMapper objectMapper = new ObjectMapper(); 23 | 24 | @Test 25 | void extractAllPlayersAsCsv() throws IOException { 26 | System.out.println("Team;First Name;Last Name;French;Drafted;Id"); 27 | HttpResponse response = QueryManager.getHttpResponse(SOURCE_BASE_URL + "/" + CURRENT_SEASON + "/players.json"); 28 | LeaguePlayersResponse leaguePlayers = objectMapper.readValue(response.getEntity().getContent(), LeaguePlayersResponse.class); 29 | 30 | List players = leaguePlayers.getLeague().getStandard(); 31 | players.stream().filter(p -> getTeam(p) != null).map(toCsv()).forEach(System.out::println); 32 | } 33 | 34 | private TeamName getTeam(LeaguePlayer p) { 35 | return NbaTeamIds.findById(p.getTeamId()); 36 | } 37 | 38 | private Function toCsv() { 39 | return p -> 40 | String.join( 41 | ";", 42 | getTeam(p).nickname(), 43 | p.getFirstName(), 44 | p.getLastName(), 45 | frenchPlayer(p), 46 | currentDraft(p.getDraft()), 47 | p.getPersonId() 48 | ); 49 | } 50 | 51 | private String frenchPlayer(LeaguePlayer p) { 52 | return "FRANCE".equalsIgnoreCase(p.getCountry()) ? "1" : ""; 53 | } 54 | 55 | private String currentDraft(Draft draft) { 56 | if (String.valueOf(CURRENT_SEASON).equalsIgnoreCase(draft.getSeasonYear())) { 57 | return draft.getPickNum(); 58 | } 59 | 60 | return ""; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiNbaGames.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import fr.hippo.nbastats.domain.GameStat; 4 | import fr.hippo.nbastats.domain.Games; 5 | import java.time.LocalDate; 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | import java.util.List; 9 | import java.util.Optional; 10 | import java.util.function.Function; 11 | import java.util.function.Predicate; 12 | import java.util.stream.Collectors; 13 | import java.util.stream.Stream; 14 | import javax.annotation.PostConstruct; 15 | import org.springframework.stereotype.Repository; 16 | 17 | @Repository 18 | class ApiNbaGames implements Games { 19 | 20 | static final int STATUS_FINISHED = 3; 21 | private List released = new ArrayList<>(); 22 | 23 | private final ApiScoreboards scoreboards; 24 | private final ReleasedGames releasedGames; 25 | private final ApiNbaGameConverter gameConverter; 26 | 27 | ApiNbaGames(ApiScoreboards scoreboards, ReleasedGames releasedGames, ApiNbaGameConverter gameConverter) { 28 | this.scoreboards = scoreboards; 29 | this.releasedGames = releasedGames; 30 | this.gameConverter = gameConverter; 31 | } 32 | 33 | @PostConstruct 34 | void initReleases() { 35 | released = releasedGames.findAll(); 36 | } 37 | 38 | @Override 39 | public Optional findUnreleased() { 40 | return getRecentGames().stream().filter(finished()).filter(notReleased()).findFirst().map(release()); 41 | } 42 | 43 | private List getRecentGames() { 44 | return Stream 45 | .of(scoreboards.forDate(LocalDate.now().minusDays(1)), scoreboards.forDate(LocalDate.now())) 46 | .map(ApiScoreboard::getGames) 47 | .flatMap(Collection::stream) 48 | .collect(Collectors.toList()); 49 | } 50 | 51 | private Predicate finished() { 52 | return game -> game.getGameStatus() == STATUS_FINISHED; 53 | } 54 | 55 | private Predicate notReleased() { 56 | return game -> !released.contains(game.getGameId()); 57 | } 58 | 59 | private Function release() { 60 | return game -> { 61 | storeRelease(game.getGameId()); 62 | 63 | return gameConverter.toDomain(game, scoreboards.boxscoreForGame(game.getGameId())); 64 | }; 65 | } 66 | 67 | private void storeRelease(String gameId) { 68 | releasedGames.add(gameId); 69 | released.add(gameId); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/domain/TeamName.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | public enum TeamName { 6 | BOSTON("Boston", "Celtics", "Boston Celtics"), 7 | BROOKLYN("Brooklyn", "Nets", "Brooklyn Nets"), 8 | NEW_YORK("New York", "Knicks", "New York Knicks"), 9 | PHILADELPHIA("Philadelphia 76", "Sixers", "Philadelphia 76ers"), 10 | TORONTO("Toronto", "Raptors", "Toronto Raptors"), 11 | CHICAGO("Chicago", "Bulls", "Chicago Bulls"), 12 | CLEVELAND("Cleveland", "Cavaliers", "Cleveland Cavaliers"), 13 | DETROIT("Detroit", "Pistons", "Detroit Pistons"), 14 | INDIANA("Indiana", "Pacers", "Indiana Pacers"), 15 | MILWAUKEE("Milwaukee", "Bucks", "Milwaukee Bucks"), 16 | ATLANTA("Atlanta", "Hawks", "Atlanta Hawks"), 17 | CHARLOTTE("Charlotte", "Hornets", "Charlotte Hornets"), 18 | MIAMI("Miami", "Heat", "Miami Heat"), 19 | ORLANDO("Orlando", "Magic", "Orlando Magic"), 20 | WASHINGTON("Washington", "Wizards", "Washington Wizards"), 21 | DENVER("Denver", "Nuggets", "Denver Nuggets"), 22 | MINNESOTA("Minnesota", "Timberwolves", "Minnesota Timberwolves"), 23 | OKLAHOMA("Oklahoma City", "Thunder", "Oklahoma City Thunder"), 24 | PORTLAND("Portland", "Blazers", "Portland Trail Blazers"), 25 | UTAH("Salt Lake City", "Jazz", "Utah Jazz"), 26 | GOLDEN_STATE("San Francisco", "Warriors", "Golden State Warriors"), 27 | CLIPPERS("Los Angeles", "Clippers", "Los Angeles Clippers"), 28 | LAKERS("Los Angeles", "Lakers", "Los Angeles Lakers"), 29 | PHOENIX("Phoenix", "Suns", "Phoenix Suns"), 30 | SACRAMENTO("Sacramento", "Kings", "Sacramento Kings"), 31 | DALLAS("Dallas", "Mavericks", "Dallas Mavericks"), 32 | HOUSTON("Houston", "Rockets", "Houston Rockets"), 33 | MEMPHIS("Memphis", "Grizzlies", "Memphis Grizzlies"), 34 | NEW_ORLEANS("New Orleans", "Pelicans", "New Orleans Pelicans"), 35 | SAN_ANTONIO("San Antonio", "Spurs", "San Antonio Spurs"); 36 | 37 | private final String city; 38 | private final String nickname; 39 | private final String fullName; 40 | 41 | TeamName(String city, String nickname, String fullName) { 42 | this.city = city; 43 | this.nickname = nickname; 44 | this.fullName = fullName; 45 | } 46 | 47 | public String city() { 48 | return city; 49 | } 50 | 51 | public String nickname() { 52 | return nickname; 53 | } 54 | 55 | public String fullName() { 56 | return fullName; 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return StringUtils.center(" " + fullName + " ", 29, "-"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '43 22 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'java' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Set up JDK 1.11 51 | - name: Set up JDK 1.11 52 | uses: actions/setup-java@v1 53 | with: 54 | java-version: '11.x' 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v1 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 https://git.io/JvXDl 63 | 64 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 65 | # and modify them (or add more) to build your code if your project 66 | # uses a compiled language 67 | 68 | #- run: | 69 | # make bootstrap 70 | # make release 71 | 72 | - name: Perform CodeQL Analysis 73 | uses: github/codeql-action/analyze@v1 74 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiNbaGamesUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | import static org.mockito.Mockito.*; 5 | 6 | import fr.hippo.nbastats.domain.GameStatUnitTest; 7 | import java.time.LocalDate; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.extension.ExtendWith; 14 | import org.mockito.InjectMocks; 15 | import org.mockito.Mock; 16 | import org.springframework.test.context.junit.jupiter.SpringExtension; 17 | 18 | @ExtendWith(SpringExtension.class) 19 | class ApiNbaGamesUnitTest { 20 | 21 | @Mock 22 | private ApiScoreboards scoreboards; 23 | 24 | @Mock 25 | private ReleasedGames releasedGames; 26 | 27 | @Mock 28 | private ApiNbaGameConverter gameConverter; 29 | 30 | @InjectMocks 31 | private ApiNbaGames games; 32 | 33 | @BeforeEach 34 | private void mockScoreboard() { 35 | LocalDate yesterday = LocalDate.now().minusDays(1); 36 | when(scoreboards.forDate(yesterday)).thenReturn(buildScoreboard(game("01", 3), game("02", 3))); 37 | 38 | LocalDate today = LocalDate.now(); 39 | when(scoreboards.forDate(today)).thenReturn(buildScoreboard(game("11", 2), game("12", 3), game("13", 1))); 40 | } 41 | 42 | private ApiScoreboard buildScoreboard(ApiModule... modules) { 43 | ApiScoreboard scoreboard = new ApiScoreboard(); 44 | scoreboard.setModules(Arrays.asList(modules)); 45 | return scoreboard; 46 | } 47 | 48 | @BeforeEach 49 | private void mockBoxscores() { 50 | when(scoreboards.boxscoreForGame("02")).thenReturn(new ApiBoxscoreGame()); 51 | when(scoreboards.boxscoreForGame("12")).thenReturn(new ApiBoxscoreGame()); 52 | } 53 | 54 | @BeforeEach 55 | private void mockGameConverter() { 56 | when(gameConverter.toDomain(any(ApiScoreboardGame.class), any(ApiBoxscoreGame.class))) 57 | .thenReturn(GameStatUnitTest.defaultGameStat()); 58 | } 59 | 60 | @BeforeEach 61 | private void mockReleasedGames() { 62 | when(releasedGames.findAll()).thenReturn(new ArrayList<>(List.of("01"))); 63 | games.initReleases(); 64 | } 65 | 66 | @Test 67 | void shouldFindTwoUnreleasedGames() { 68 | release(); 69 | release(); 70 | 71 | assertThat(games.findUnreleased()).isEmpty(); 72 | verify(releasedGames).add("02"); 73 | verify(releasedGames).add("12"); 74 | } 75 | 76 | private void release() { 77 | assertThat(games.findUnreleased()).isPresent(); 78 | } 79 | 80 | private ApiModule game(String gameId, int status) { 81 | ApiScoreboardGame gameDetails = new ApiScoreboardGame(); 82 | gameDetails.setGameId(gameId); 83 | gameDetails.setGameStatus(status); 84 | 85 | ApiCard card = new ApiCard(); 86 | card.setCardData(gameDetails); 87 | 88 | ApiModule module = new ApiModule(); 89 | module.setCards(List.of(card)); 90 | return module; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.2.2.RELEASE 9 | 10 | 11 | fr.hippo 12 | nba-stats 13 | 0.0.1-SNAPSHOT 14 | nba-stats 15 | Send NBA Stats to Telegram channel 16 | 17 | 18 | 11 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-test 30 | test 31 | 32 | 33 | org.junit.vintage 34 | junit-vintage-engine 35 | 36 | 37 | 38 | 39 | 40 | org.apache.commons 41 | commons-lang3 42 | 3.9 43 | 44 | 45 | 46 | com.antoineguay 47 | nbawrapper 48 | 0.0.6 49 | test 50 | 51 | 52 | 53 | 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-maven-plugin 58 | 59 | true 60 | 61 | 62 | 63 | org.jacoco 64 | jacoco-maven-plugin 65 | 0.8.5 66 | 67 | 68 | 69 | prepare-agent 70 | 71 | 72 | 73 | report 74 | test 75 | 76 | report 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/domain/TeamStatUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import static fr.hippo.nbastats.domain.PlayerStatUnitTest.*; 4 | import static org.assertj.core.api.Assertions.*; 5 | 6 | import java.util.List; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class TeamStatUnitTest { 10 | 11 | @Test 12 | void shouldNotBuildWithoutFilter() { 13 | assertThatThrownBy(() -> fullTeamStat().filter(null).build()) 14 | .isExactlyInstanceOf(IllegalArgumentException.class) 15 | .hasMessageContaining("filter"); 16 | } 17 | 18 | @Test 19 | void shouldNotBuildWithoutName() { 20 | assertThatThrownBy(() -> fullTeamStat().name(null).build()) 21 | .isExactlyInstanceOf(IllegalArgumentException.class) 22 | .hasMessageContaining("name"); 23 | } 24 | 25 | @Test 26 | void shouldNotBuildWithoutPlayers() { 27 | assertThatThrownBy(() -> fullTeamStat().players(null).build()) 28 | .isExactlyInstanceOf(IllegalArgumentException.class) 29 | .hasMessageContaining("players"); 30 | } 31 | 32 | @Test 33 | void shouldBuild() { 34 | TeamStat teamStat = detroit(); 35 | assertThat(teamStat.getName()).isEqualTo(TeamName.DETROIT); 36 | assertThat(teamStat.getScore()).isEqualTo(124); 37 | assertThat(teamStat.getWins()).isEqualTo(12); 38 | assertThat(teamStat.getLosses()).isEqualTo(13); 39 | } 40 | 41 | @Test 42 | void shouldGetUnmodifiablePlayers() { 43 | assertThatThrownBy(() -> detroit().getPlayers().clear()).isExactlyInstanceOf(UnsupportedOperationException.class); 44 | } 45 | 46 | @Test 47 | void shouldGetPlayersOrderedByEvaluation() { 48 | List players = detroit().getPlayers(); 49 | 50 | assertThat(players) 51 | .extracting(PlayerStat::toString) 52 | .containsExactly( 53 | brookLopez().toString(), 54 | jeremyLamb().toString(), 55 | moBamba().toString(), 56 | codyZeller().toString(), 57 | didNotPlayed().toString() 58 | ); 59 | } 60 | 61 | @Test 62 | void shouldHaveFullToString() { 63 | assertThat(detroit()) 64 | .hasToString( 65 | "------ Detroit Pistons ------\n" + brookLopez() + "\n\n" + jeremyLamb() + "\n\n" + moBamba() + "\n\n" + codyZeller() 66 | ); 67 | } 68 | 69 | @Test 70 | void shouldHaveFilteredToString() { 71 | assertThat(detroitLowEvalFiltered()) 72 | .hasToString( 73 | "------ Detroit Pistons ------\n" + brookLopez() + "\n\n" + jeremyLamb() + "\n\n" + moBamba() + "\n\n" + codyZeller() 74 | ); 75 | } 76 | 77 | @Test 78 | void shouldHaveAtLeast3PlayersToString() { 79 | PlayerStat notWantedPlayer = brookLopez(); 80 | assertThat(detroitHighEvalFiltered()) 81 | .hasToString("------ Detroit Pistons ------\n" + notWantedPlayer + "\n\n" + moBamba() + "\n\n" + codyZeller()); 82 | } 83 | 84 | static TeamStat detroit() { 85 | return fullTeamStat().build(); 86 | } 87 | 88 | private TeamStat detroitLowEvalFiltered() { 89 | return fullTeamStat().filter(new StatFilter(20, List.of(MO_BAMBA_ID, CODY_ZELLER_ID, NOT_PLAYING_ID))).build(); 90 | } 91 | 92 | private TeamStat detroitHighEvalFiltered() { 93 | return fullTeamStat().filter(new StatFilter(100, List.of(MO_BAMBA_ID, CODY_ZELLER_ID, NOT_PLAYING_ID))).build(); 94 | } 95 | 96 | static TeamStat indiana() { 97 | return fullTeamStat().name(TeamName.INDIANA).score(123).wins(8).losses(18).build(); 98 | } 99 | 100 | private static TeamStat.TeamStatBuilder fullTeamStat() { 101 | return TeamStat.builder().filter(emptyFilter()).name(TeamName.DETROIT).score(124).wins(12).losses(13).players(players()); 102 | } 103 | 104 | private static StatFilter emptyFilter() { 105 | return StatFilterUnitTest.empty(); 106 | } 107 | 108 | private static List players() { 109 | return List.of(jeremyLamb(), didNotPlayed(), brookLopez(), moBamba(), codyZeller()); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/domain/TeamStat.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.Comparator; 6 | import java.util.List; 7 | import java.util.function.Predicate; 8 | import java.util.stream.Collectors; 9 | import java.util.stream.Stream; 10 | import org.springframework.util.Assert; 11 | 12 | public class TeamStat { 13 | 14 | private static final Comparator PLAYER_STAT_COMPARATOR = Comparator.comparing(PlayerStat::eval).reversed(); 15 | 16 | private final StatFilter filter; 17 | private final TeamName name; 18 | private final int score; 19 | private final int wins; 20 | private final int losses; 21 | private final List players; 22 | 23 | TeamStat(TeamStatBuilder builder) { 24 | Assert.notNull(builder.filter, "missing filter"); 25 | Assert.notNull(builder.name, "missing name"); 26 | Assert.notNull(builder.players, "missing players"); 27 | 28 | this.filter = builder.filter; 29 | this.name = builder.name; 30 | this.score = builder.score; 31 | this.wins = builder.wins; 32 | this.losses = builder.losses; 33 | this.players = buildPlayers(builder.players); 34 | } 35 | 36 | private List buildPlayers(List players) { 37 | List playerStats = new ArrayList<>(players); 38 | playerStats.sort(PLAYER_STAT_COMPARATOR); 39 | return Collections.unmodifiableList(playerStats); 40 | } 41 | 42 | public static TeamStatBuilder builder() { 43 | return new TeamStatBuilder(); 44 | } 45 | 46 | TeamName getName() { 47 | return name; 48 | } 49 | 50 | int getScore() { 51 | return score; 52 | } 53 | 54 | public int getWins() { 55 | return wins; 56 | } 57 | 58 | public int getLosses() { 59 | return losses; 60 | } 61 | 62 | List getPlayers() { 63 | return players; 64 | } 65 | 66 | @Override 67 | public String toString() { 68 | String playerStats = wantedPlayers().stream().map(PlayerStat::toString).collect(Collectors.joining("\n\n")); 69 | return name + "\n" + playerStats; 70 | } 71 | 72 | private List wantedPlayers() { 73 | List wantedPlayers = players.stream().filter(PlayerStat::played).filter(matchFilter()).collect(Collectors.toList()); 74 | 75 | if (wantedPlayers.size() >= 3) { 76 | return wantedPlayers; 77 | } 78 | 79 | List notWantedPlayers = players.stream().filter(p -> !wantedPlayers.contains(p)).collect(Collectors.toList()); 80 | 81 | List playersToAdd = notWantedPlayers.subList(0, 3 - wantedPlayers.size()); 82 | 83 | return Stream.concat(wantedPlayers.stream(), playersToAdd.stream()).sorted(PLAYER_STAT_COMPARATOR).collect(Collectors.toList()); 84 | } 85 | 86 | private Predicate matchFilter() { 87 | return playerStat -> filter.matches(playerStat.eval(), playerStat.id()); 88 | } 89 | 90 | public static class TeamStatBuilder { 91 | 92 | private StatFilter filter; 93 | private TeamName name; 94 | private int score; 95 | private int wins; 96 | private int losses; 97 | private List players; 98 | 99 | public TeamStatBuilder filter(StatFilter filter) { 100 | this.filter = filter; 101 | 102 | return this; 103 | } 104 | 105 | public TeamStatBuilder name(TeamName name) { 106 | this.name = name; 107 | 108 | return this; 109 | } 110 | 111 | public TeamStatBuilder score(int score) { 112 | this.score = score; 113 | 114 | return this; 115 | } 116 | 117 | public TeamStatBuilder wins(int wins) { 118 | this.wins = wins; 119 | 120 | return this; 121 | } 122 | 123 | public TeamStatBuilder losses(int losses) { 124 | this.losses = losses; 125 | 126 | return this; 127 | } 128 | 129 | public TeamStatBuilder players(List players) { 130 | this.players = players; 131 | 132 | return this; 133 | } 134 | 135 | public TeamStat build() { 136 | return new TeamStat(this); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.5"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/infrastructure/secondary/api/ApiNbaGameConverterUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.infrastructure.secondary.api; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | import static org.mockito.Mockito.*; 5 | 6 | import fr.hippo.nbastats.JsonHelper; 7 | import fr.hippo.nbastats.config.StatFilterProperties; 8 | import fr.hippo.nbastats.domain.GameStat; 9 | import fr.hippo.nbastats.domain.StatFilterUnitTest; 10 | import java.io.IOException; 11 | import java.nio.file.Files; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.mockito.InjectMocks; 16 | import org.mockito.Mock; 17 | import org.springframework.beans.factory.annotation.Value; 18 | import org.springframework.core.io.Resource; 19 | import org.springframework.test.context.junit.jupiter.SpringExtension; 20 | 21 | @ExtendWith(SpringExtension.class) 22 | class ApiNbaGameConverterUnitTest { 23 | 24 | @Value("classpath:fixtures/api-scoreboard.json") 25 | private Resource scoreboardResource; 26 | 27 | @Value("classpath:fixtures/api-boxscore.json") 28 | private Resource boxscoreResource; 29 | 30 | @Mock 31 | private StatFilterProperties statFilterProperties; 32 | 33 | @InjectMocks 34 | private ApiNbaGameConverter converter; 35 | 36 | private ApiScoreboardGame scoreboardGame; 37 | private ApiBoxscoreGame boxscoreGame; 38 | 39 | @BeforeEach 40 | void initMocks() throws IOException { 41 | scoreboardGame = getScoreboardGame(); 42 | boxscoreGame = getBoxscoreGame(); 43 | when(statFilterProperties.statFilter()).thenReturn(StatFilterUnitTest.empty()); 44 | } 45 | 46 | @Test 47 | void shouldConvertGame() { 48 | GameStat gameStat = converter.toDomain(scoreboardGame, boxscoreGame); 49 | 50 | assertThat(gameStat) 51 | .hasToString( 52 | " Magic 109 - Pistons 113 \n" + 53 | " 0-1 1-0 \n" + 54 | "\n" + 55 | "------- Orlando Magic -------\n" + 56 | "P. Banchero 46|27 9 5 2 0\n" + 57 | "11/18 0/0 5/7 | 4|35'\n" + 58 | "\n" + 59 | "J. Suggs *31|21 1 3 0 2\n" + 60 | " 8/11 4/6 1/1 | 4|25'\n" + 61 | "\n" + 62 | "W. Carter J 26|11 11 3 1 1\n" + 63 | " 5/8 1/2 0/0 | 3|33'\n" + 64 | "\n" + 65 | "F. Wagner 22|20 4 5 0 0\n" + 66 | " 8/18 2/6 2/2 | 5|34'\n" + 67 | "\n" + 68 | "B. Bol 19|10 6 0 1 0\n" + 69 | " 4/6 0/0 2/2 | 2|18'\n" + 70 | "\n" + 71 | "R. Hampton 15| 5 4 2 0 1\n" + 72 | " 1/2 1/1 2/2 | 0|11'\n" + 73 | "\n" + 74 | "T. Ross 14|13 4 1 0 1\n" + 75 | " 5/12 3/8 0/1 | 0|32'\n" + 76 | "\n" + 77 | "C. Okeke 5| 2 4 1 0 0\n" + 78 | " 0/3 0/1 2/2 | 0|18'\n" + 79 | "\n" + 80 | "C. Houstan -1| 0 3 0 1 0\n" + 81 | " 0/3 0/2 0/0 | 0|22'\n" + 82 | "\n" + 83 | "M. Bamba -8| 0 2 1 0 0\n" + 84 | " 0/5 0/4 0/2 | 0|12'\n" + 85 | "\n" + 86 | "------ Detroit Pistons ------\n" + 87 | "B. Bogdanov 35|24 5 2 0 1\n" + 88 | " 8/16 6/10 2/2 | 1|35'\n" + 89 | "\n" + 90 | "J. Ivey 29|19 3 4 0 3\n" + 91 | " 8/15 2/4 1/1 | 2|32'\n" + 92 | "\n" + 93 | "I. Stewart 28|14 5 3 0 2\n" + 94 | " 3/6 1/4 7/8 | 0|26'\n" + 95 | "\n" + 96 | "C. Cunningh 25|18 1 10 0 1\n" + 97 | " 6/16 2/6 4/4 | 3|35'\n" + 98 | "\n" + 99 | "J. Duren 22|14 10 1 3 0\n" + 100 | " 7/13 0/0 0/4 | 3|22'\n" + 101 | "\n" + 102 | "S. Bey 18| 8 6 3 0 1\n" + 103 | " 2/5 0/2 4/4 | 1|31'\n" + 104 | "\n" + 105 | "C. Joseph 16| 8 1 2 0 0\n" + 106 | " 3/3 2/2 0/0 | 0|15'\n" + 107 | "\n" + 108 | "K. Hayes 8| 3 5 5 1 3\n" + 109 | " 1/9 0/3 1/1 | 0|16'\n" + 110 | "\n" + 111 | "H. Diallo 3| 2 2 1 0 0\n" + 112 | " 1/3 0/1 0/0 | 0|15'\n" + 113 | "\n" + 114 | "K. Knox II -6| 3 3 0 0 0\n" + 115 | " 1/8 1/6 0/0 | 2|13'" 116 | ); 117 | } 118 | 119 | private ApiBoxscoreGame getBoxscoreGame() throws IOException { 120 | return JsonHelper.readFromJson(defaultBoxscoreJson(), ApiBoxscore.class).getGame(); 121 | } 122 | 123 | private ApiScoreboardGame getScoreboardGame() throws IOException { 124 | return JsonHelper.readFromJson(defaultScoreboardJson(), ApiScoreboard.class).getGames().iterator().next(); 125 | } 126 | 127 | private String defaultBoxscoreJson() throws IOException { 128 | return new String(Files.readAllBytes(boxscoreResource.getFile().toPath())); 129 | } 130 | 131 | private String defaultScoreboardJson() throws IOException { 132 | return new String(Files.readAllBytes(scoreboardResource.getFile().toPath())); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/test/java/fr/hippo/nbastats/domain/PlayerStatUnitTest.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class PlayerStatUnitTest { 8 | 9 | static final int JEREMY_LAMB_ID = 10; 10 | static final int MO_BAMBA_ID = 11; 11 | static final int CODY_ZELLER_ID = 12; 12 | static final int NOT_PLAYING_ID = 13; 13 | 14 | @Test 15 | void shouldNotBuildWithoutIdentity() { 16 | assertThatThrownBy(() -> fullPlayerStat().identity(null).build()) 17 | .isExactlyInstanceOf(IllegalArgumentException.class) 18 | .hasMessageContaining("identity"); 19 | } 20 | 21 | @Test 22 | void shouldNotBuildWithoutFouls() { 23 | assertThatThrownBy(() -> fullPlayerStat().fouls(null).build()) 24 | .isExactlyInstanceOf(IllegalArgumentException.class) 25 | .hasMessageContaining("fouls"); 26 | } 27 | 28 | @Test 29 | void shouldNotBuildWithoutPoints() { 30 | assertThatThrownBy(() -> fullPlayerStat().points(null).build()) 31 | .isExactlyInstanceOf(IllegalArgumentException.class) 32 | .hasMessageContaining("points"); 33 | } 34 | 35 | @Test 36 | void shouldNotBuildWithoutRebounds() { 37 | assertThatThrownBy(() -> fullPlayerStat().rebounds(null).build()) 38 | .isExactlyInstanceOf(IllegalArgumentException.class) 39 | .hasMessageContaining("rebounds"); 40 | } 41 | 42 | @Test 43 | void shouldNotBuildWithoutAssists() { 44 | assertThatThrownBy(() -> fullPlayerStat().assists(null).build()) 45 | .isExactlyInstanceOf(IllegalArgumentException.class) 46 | .hasMessageContaining("assists"); 47 | } 48 | 49 | @Test 50 | void shouldNotBuildWithoutBlocks() { 51 | assertThatThrownBy(() -> fullPlayerStat().blocks(null).build()) 52 | .isExactlyInstanceOf(IllegalArgumentException.class) 53 | .hasMessageContaining("blocks"); 54 | } 55 | 56 | @Test 57 | void shouldNotBuildWithoutSteals() { 58 | assertThatThrownBy(() -> fullPlayerStat().steals(null).build()) 59 | .isExactlyInstanceOf(IllegalArgumentException.class) 60 | .hasMessageContaining("steals"); 61 | } 62 | 63 | @Test 64 | void shouldNotBuildWithoutFieldGoals() { 65 | assertThatThrownBy(() -> fullPlayerStat().fieldGoals(null).build()) 66 | .isExactlyInstanceOf(IllegalArgumentException.class) 67 | .hasMessageContaining("fieldGoals"); 68 | } 69 | 70 | @Test 71 | void shouldNotBuildWithoutThreePoints() { 72 | assertThatThrownBy(() -> fullPlayerStat().threePoints(null).build()) 73 | .isExactlyInstanceOf(IllegalArgumentException.class) 74 | .hasMessageContaining("threePoints"); 75 | } 76 | 77 | @Test 78 | void shouldNotBuildWithoutFreeThrows() { 79 | assertThatThrownBy(() -> fullPlayerStat().freeThrows(null).build()) 80 | .isExactlyInstanceOf(IllegalArgumentException.class) 81 | .hasMessageContaining("freeThrows"); 82 | } 83 | 84 | @Test 85 | void shouldNotBuildWithoutTurnovers() { 86 | assertThatThrownBy(() -> fullPlayerStat().turnovers(null).build()) 87 | .isExactlyInstanceOf(IllegalArgumentException.class) 88 | .hasMessageContaining("turnovers"); 89 | } 90 | 91 | @Test 92 | void shouldNotBuildWithoutMinutes() { 93 | assertThatThrownBy(() -> fullPlayerStat().minutes(null).build()) 94 | .isExactlyInstanceOf(IllegalArgumentException.class) 95 | .hasMessageContaining("minutes"); 96 | } 97 | 98 | @Test 99 | void shouldGetEvaluation() { 100 | assertThat(brookLopez().eval()).isEqualTo(76); 101 | } 102 | 103 | @Test 104 | void shouldGetPlayedWithMinutes() { 105 | assertThat(brookLopez().played()).isTrue(); 106 | } 107 | 108 | @Test 109 | void shouldGetNotPlayedWithNoMinute() { 110 | assertThat(didNotPlayed().played()).isFalse(); 111 | } 112 | 113 | @Test 114 | void shouldHaveFullToString() { 115 | assertThat(brookLopez()).hasToString("B. Lopez *76|42 8 10 4 0\n 9/12 10/13 4/5 | 4|34'"); 116 | } 117 | 118 | @Test 119 | void shouldGetId() { 120 | assertThat(moBamba().id()).isEqualTo(MO_BAMBA_ID); 121 | } 122 | 123 | public static PlayerStat brookLopez() { 124 | return fullPlayerStat().build(); 125 | } 126 | 127 | public static PlayerStat jeremyLamb() { 128 | return fullPlayerStat().identity(new Identity(JEREMY_LAMB_ID, "Jeremy", "Lamb")).points(new Stat(20)).build(); 129 | } 130 | 131 | static PlayerStat moBamba() { 132 | return fullPlayerStat().identity(new Identity(MO_BAMBA_ID, "Mo", "Bamba")).points(new Stat(10)).build(); 133 | } 134 | 135 | static PlayerStat codyZeller() { 136 | return fullPlayerStat().identity(new Identity(CODY_ZELLER_ID, "Cody", "Zeller")).points(new Stat(0)).build(); 137 | } 138 | 139 | static PlayerStat didNotPlayed() { 140 | return fullPlayerStat() 141 | .identity(new Identity(NOT_PLAYING_ID, "Not", "Playing")) 142 | .points(new Stat(0)) 143 | .rebounds(new Stat(0)) 144 | .minutes(new Stat(0)) 145 | .build(); 146 | } 147 | 148 | static PlayerStat.PlayerStatBuilder fullPlayerStat() { 149 | return PlayerStat 150 | .builder() 151 | .identity(IdentityUnitTest.defaultIdentity()) 152 | .fouls(new Fouls(6)) 153 | .points(new Stat(42)) 154 | .rebounds(new Stat(8)) 155 | .assists(new Stat(10)) 156 | .blocks(new Stat(4)) 157 | .steals(new Stat(0)) 158 | .fieldGoals(new StatTuple(9, 12)) 159 | .threePoints(new StatTuple(10, 13)) 160 | .freeThrows(new StatTuple(4, 5)) 161 | .turnovers(new Stat(4)) 162 | .minutes(new Stat(34)); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/fr/hippo/nbastats/domain/PlayerStat.java: -------------------------------------------------------------------------------- 1 | package fr.hippo.nbastats.domain; 2 | 3 | import org.springframework.util.Assert; 4 | 5 | public class PlayerStat { 6 | 7 | private final Identity identity; 8 | private final Fouls fouls; 9 | private final Evaluation evaluation; 10 | private final Stat points; 11 | private final Stat rebounds; 12 | private final Stat assists; 13 | private final Stat blocks; 14 | private final Stat steals; 15 | private final StatTuple fieldGoals; 16 | private final StatTuple threePoints; 17 | private final StatTuple freeThrows; 18 | private final Stat turnovers; 19 | private final Stat minutes; 20 | 21 | private PlayerStat(PlayerStatBuilder builder) { 22 | Assert.notNull(builder.identity, "missing identity"); 23 | Assert.notNull(builder.fouls, "missing fouls"); 24 | Assert.notNull(builder.points, "missing points"); 25 | Assert.notNull(builder.rebounds, "missing rebounds"); 26 | Assert.notNull(builder.assists, "missing assists"); 27 | Assert.notNull(builder.blocks, "missing blocks"); 28 | Assert.notNull(builder.steals, "missing steals"); 29 | Assert.notNull(builder.fieldGoals, "missing fieldGoals"); 30 | Assert.notNull(builder.threePoints, "missing threePoints"); 31 | Assert.notNull(builder.freeThrows, "missing freeThrows"); 32 | Assert.notNull(builder.turnovers, "missing turnovers"); 33 | Assert.notNull(builder.minutes, "missing minutes"); 34 | 35 | this.identity = builder.identity; 36 | this.fouls = builder.fouls; 37 | this.points = builder.points; 38 | this.rebounds = builder.rebounds; 39 | this.assists = builder.assists; 40 | this.blocks = builder.blocks; 41 | this.steals = builder.steals; 42 | this.fieldGoals = builder.fieldGoals; 43 | this.threePoints = builder.threePoints; 44 | this.freeThrows = builder.freeThrows; 45 | this.turnovers = builder.turnovers; 46 | this.minutes = builder.minutes; 47 | 48 | this.evaluation = ttflEvaluation(); 49 | } 50 | 51 | private Evaluation ttflEvaluation() { 52 | return new Evaluation(goodMovesScore() - badMovesScore()); 53 | } 54 | 55 | private int goodMovesScore() { 56 | return ( 57 | points.value() + 58 | rebounds.value() + 59 | assists.value() + 60 | blocks.value() + 61 | steals.value() + 62 | fieldGoals.getSuccess() + 63 | threePoints.getSuccess() + 64 | freeThrows.getSuccess() 65 | ); 66 | } 67 | 68 | private int badMovesScore() { 69 | return turnovers.value() + fieldGoals.getMissed() + threePoints.getMissed() + freeThrows.getMissed(); 70 | } 71 | 72 | public static PlayerStatBuilder builder() { 73 | return new PlayerStatBuilder(); 74 | } 75 | 76 | int eval() { 77 | return evaluation.value(); 78 | } 79 | 80 | boolean played() { 81 | return minutes.value() > 0; 82 | } 83 | 84 | int id() { 85 | return identity.getId(); 86 | } 87 | 88 | @Override 89 | public String toString() { 90 | return line1() + "\n" + line2(); 91 | } 92 | 93 | private String line1() { 94 | return new StringBuilder() 95 | .append(identity) 96 | .append(fouls) 97 | .append(evaluation) 98 | .append("|") 99 | .append(points) 100 | .append(" ") 101 | .append(rebounds) 102 | .append(" ") 103 | .append(assists) 104 | .append(" ") 105 | .append(blocks) 106 | .append(" ") 107 | .append(steals) 108 | .toString(); 109 | } 110 | 111 | private String line2() { 112 | return new StringBuilder() 113 | .append(fieldGoals) 114 | .append(" ") 115 | .append(threePoints) 116 | .append(" ") 117 | .append(freeThrows) 118 | .append("|") 119 | .append(turnovers) 120 | .append("|") 121 | .append(minutes) 122 | .append("'") 123 | .toString(); 124 | } 125 | 126 | public static class PlayerStatBuilder { 127 | 128 | private Identity identity; 129 | private Fouls fouls; 130 | private Stat points; 131 | private Stat rebounds; 132 | private Stat assists; 133 | private Stat blocks; 134 | private Stat steals; 135 | private StatTuple fieldGoals; 136 | private StatTuple threePoints; 137 | private StatTuple freeThrows; 138 | private Stat turnovers; 139 | private Stat minutes; 140 | 141 | public PlayerStatBuilder identity(Identity identity) { 142 | this.identity = identity; 143 | 144 | return this; 145 | } 146 | 147 | public PlayerStatBuilder fouls(Fouls fouls) { 148 | this.fouls = fouls; 149 | 150 | return this; 151 | } 152 | 153 | public PlayerStatBuilder points(Stat points) { 154 | this.points = points; 155 | 156 | return this; 157 | } 158 | 159 | public PlayerStatBuilder rebounds(Stat rebounds) { 160 | this.rebounds = rebounds; 161 | 162 | return this; 163 | } 164 | 165 | public PlayerStatBuilder assists(Stat assists) { 166 | this.assists = assists; 167 | 168 | return this; 169 | } 170 | 171 | public PlayerStatBuilder blocks(Stat blocks) { 172 | this.blocks = blocks; 173 | 174 | return this; 175 | } 176 | 177 | public PlayerStatBuilder steals(Stat steals) { 178 | this.steals = steals; 179 | 180 | return this; 181 | } 182 | 183 | public PlayerStatBuilder fieldGoals(StatTuple fieldGoals) { 184 | this.fieldGoals = fieldGoals; 185 | 186 | return this; 187 | } 188 | 189 | public PlayerStatBuilder threePoints(StatTuple threePoints) { 190 | this.threePoints = threePoints; 191 | 192 | return this; 193 | } 194 | 195 | public PlayerStatBuilder freeThrows(StatTuple freeThrows) { 196 | this.freeThrows = freeThrows; 197 | 198 | return this; 199 | } 200 | 201 | public PlayerStatBuilder turnovers(Stat turnover) { 202 | this.turnovers = turnover; 203 | 204 | return this; 205 | } 206 | 207 | public PlayerStatBuilder minutes(Stat minutes) { 208 | this.minutes = minutes; 209 | 210 | return this; 211 | } 212 | 213 | public PlayerStat build() { 214 | return new PlayerStat(this); 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /src/test/resources/fixtures/api-boxscore.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "version": 1, 4 | "code": 200, 5 | "request": "http://nba.cloud/games/0022200003/boxscore?Format=json", 6 | "time": "2022-10-19 23:46:15.607257" 7 | }, 8 | "game": { 9 | "gameId": "0022200003", 10 | "gameTimeLocal": "2022-10-19T19:00:00-04:00", 11 | "gameTimeUTC": "2022-10-19T23:00:00Z", 12 | "gameTimeHome": "2022-10-19T19:00:00-04:00", 13 | "gameTimeAway": "2022-10-19T19:00:00-04:00", 14 | "gameEt": "2022-10-19T19:00:00-04:00", 15 | "duration": 136, 16 | "gameCode": "20221019/ORLDET", 17 | "gameStatusText": "Final", 18 | "gameStatus": 3, 19 | "regulationPeriods": 4, 20 | "period": 4, 21 | "gameClock": "PT00M00.00S", 22 | "attendance": 20190, 23 | "sellout": "1", 24 | "arena": { 25 | "arenaId": 624, 26 | "arenaName": "Little Caesars Arena", 27 | "arenaCity": "Detroit", 28 | "arenaState": "MI", 29 | "arenaCountry": "US", 30 | "arenaTimezone": "America/New_York" 31 | }, 32 | "officials": [ 33 | { 34 | "personId": 1151, 35 | "name": "Sean Corbin", 36 | "nameI": "S. Corbin", 37 | "firstName": "Sean", 38 | "familyName": "Corbin", 39 | "jerseyNum": "33", 40 | "assignment": "OFFICIAL2" 41 | }, 42 | { 43 | "personId": 2714, 44 | "name": "David Guthrie", 45 | "nameI": "D. Guthrie", 46 | "firstName": "David", 47 | "familyName": "Guthrie", 48 | "jerseyNum": "16", 49 | "assignment": "OFFICIAL1" 50 | }, 51 | { 52 | "personId": 1628159, 53 | "name": "Mousa Dagher", 54 | "nameI": "M. Dagher", 55 | "firstName": "Mousa", 56 | "familyName": "Dagher", 57 | "jerseyNum": "28", 58 | "assignment": "OFFICIAL3" 59 | } 60 | ], 61 | "homeTeam": { 62 | "teamId": 1610612765, 63 | "teamName": "Pistons", 64 | "teamCity": "Detroit", 65 | "teamTricode": "DET", 66 | "score": 113, 67 | "inBonus": "1", 68 | "timeoutsRemaining": 2, 69 | "periods": [ 70 | { 71 | "period": 1, 72 | "periodType": "REGULAR", 73 | "score": 17 74 | }, 75 | { 76 | "period": 2, 77 | "periodType": "REGULAR", 78 | "score": 40 79 | }, 80 | { 81 | "period": 3, 82 | "periodType": "REGULAR", 83 | "score": 34 84 | }, 85 | { 86 | "period": 4, 87 | "periodType": "REGULAR", 88 | "score": 22 89 | } 90 | ], 91 | "players": [ 92 | { 93 | "status": "ACTIVE", 94 | "order": 1, 95 | "personId": 1630180, 96 | "jerseyNum": "41", 97 | "position": "SF", 98 | "starter": "1", 99 | "oncourt": "1", 100 | "played": "1", 101 | "statistics": { 102 | "assists": 3, 103 | "blocks": 0, 104 | "blocksReceived": 0, 105 | "fieldGoalsAttempted": 5, 106 | "fieldGoalsMade": 2, 107 | "fieldGoalsPercentage": 0.4, 108 | "foulsOffensive": 0, 109 | "foulsDrawn": 2, 110 | "foulsPersonal": 2, 111 | "foulsTechnical": 0, 112 | "freeThrowsAttempted": 4, 113 | "freeThrowsMade": 4, 114 | "freeThrowsPercentage": 1.0, 115 | "minus": 73.0, 116 | "minutes": "PT31M23.00S", 117 | "minutesCalculated": "PT31M", 118 | "plus": 79.0, 119 | "plusMinusPoints": 6.0, 120 | "points": 8, 121 | "pointsFastBreak": 2, 122 | "pointsInThePaint": 4, 123 | "pointsSecondChance": 2, 124 | "reboundsDefensive": 3, 125 | "reboundsOffensive": 3, 126 | "reboundsTotal": 6, 127 | "steals": 1, 128 | "threePointersAttempted": 2, 129 | "threePointersMade": 0, 130 | "threePointersPercentage": 0.0, 131 | "turnovers": 1, 132 | "twoPointersAttempted": 3, 133 | "twoPointersMade": 2, 134 | "twoPointersPercentage": 0.666666666666666 135 | }, 136 | "name": "Saddiq Bey", 137 | "nameI": "S. Bey", 138 | "firstName": "Saddiq", 139 | "familyName": "Bey" 140 | }, 141 | { 142 | "status": "ACTIVE", 143 | "order": 2, 144 | "personId": 202711, 145 | "jerseyNum": "44", 146 | "position": "PF", 147 | "starter": "1", 148 | "oncourt": "1", 149 | "played": "1", 150 | "statistics": { 151 | "assists": 2, 152 | "blocks": 0, 153 | "blocksReceived": 2, 154 | "fieldGoalsAttempted": 16, 155 | "fieldGoalsMade": 8, 156 | "fieldGoalsPercentage": 0.5, 157 | "foulsOffensive": 0, 158 | "foulsDrawn": 5, 159 | "foulsPersonal": 3, 160 | "foulsTechnical": 0, 161 | "freeThrowsAttempted": 2, 162 | "freeThrowsMade": 2, 163 | "freeThrowsPercentage": 1.0, 164 | "minus": 85.0, 165 | "minutes": "PT34M50.00S", 166 | "minutesCalculated": "PT35M", 167 | "plus": 91.0, 168 | "plusMinusPoints": 6.0, 169 | "points": 24, 170 | "pointsFastBreak": 3, 171 | "pointsInThePaint": 4, 172 | "pointsSecondChance": 0, 173 | "reboundsDefensive": 4, 174 | "reboundsOffensive": 1, 175 | "reboundsTotal": 5, 176 | "steals": 1, 177 | "threePointersAttempted": 10, 178 | "threePointersMade": 6, 179 | "threePointersPercentage": 0.6, 180 | "turnovers": 1, 181 | "twoPointersAttempted": 6, 182 | "twoPointersMade": 2, 183 | "twoPointersPercentage": 0.333333333333333 184 | }, 185 | "name": "Bojan Bogdanovic", 186 | "nameI": "B. Bogdanovic", 187 | "firstName": "Bojan", 188 | "familyName": "Bogdanovic" 189 | }, 190 | { 191 | "status": "ACTIVE", 192 | "order": 3, 193 | "personId": 1630191, 194 | "jerseyNum": "28", 195 | "position": "C", 196 | "starter": "1", 197 | "oncourt": "1", 198 | "played": "1", 199 | "statistics": { 200 | "assists": 3, 201 | "blocks": 0, 202 | "blocksReceived": 0, 203 | "fieldGoalsAttempted": 6, 204 | "fieldGoalsMade": 3, 205 | "fieldGoalsPercentage": 0.5, 206 | "foulsOffensive": 0, 207 | "foulsDrawn": 4, 208 | "foulsPersonal": 2, 209 | "foulsTechnical": 0, 210 | "freeThrowsAttempted": 8, 211 | "freeThrowsMade": 7, 212 | "freeThrowsPercentage": 0.875, 213 | "minus": 71.0, 214 | "minutes": "PT26M18.00S", 215 | "minutesCalculated": "PT26M", 216 | "plus": 70.0, 217 | "plusMinusPoints": -1.0, 218 | "points": 14, 219 | "pointsFastBreak": 2, 220 | "pointsInThePaint": 4, 221 | "pointsSecondChance": 2, 222 | "reboundsDefensive": 5, 223 | "reboundsOffensive": 0, 224 | "reboundsTotal": 5, 225 | "steals": 2, 226 | "threePointersAttempted": 4, 227 | "threePointersMade": 1, 228 | "threePointersPercentage": 0.25, 229 | "turnovers": 0, 230 | "twoPointersAttempted": 2, 231 | "twoPointersMade": 2, 232 | "twoPointersPercentage": 1.0 233 | }, 234 | "name": "Isaiah Stewart", 235 | "nameI": "I. Stewart", 236 | "firstName": "Isaiah", 237 | "familyName": "Stewart" 238 | }, 239 | { 240 | "status": "ACTIVE", 241 | "order": 4, 242 | "personId": 1630595, 243 | "jerseyNum": "2", 244 | "position": "SG", 245 | "starter": "1", 246 | "oncourt": "1", 247 | "played": "1", 248 | "statistics": { 249 | "assists": 10, 250 | "blocks": 0, 251 | "blocksReceived": 0, 252 | "fieldGoalsAttempted": 16, 253 | "fieldGoalsMade": 6, 254 | "fieldGoalsPercentage": 0.375, 255 | "foulsOffensive": 0, 256 | "foulsDrawn": 2, 257 | "foulsPersonal": 2, 258 | "foulsTechnical": 0, 259 | "freeThrowsAttempted": 4, 260 | "freeThrowsMade": 4, 261 | "freeThrowsPercentage": 1.0, 262 | "minus": 85.0, 263 | "minutes": "PT35M06.00S", 264 | "minutesCalculated": "PT35M", 265 | "plus": 88.0, 266 | "plusMinusPoints": 3.0, 267 | "points": 18, 268 | "pointsFastBreak": 0, 269 | "pointsInThePaint": 6, 270 | "pointsSecondChance": 2, 271 | "reboundsDefensive": 1, 272 | "reboundsOffensive": 0, 273 | "reboundsTotal": 1, 274 | "steals": 1, 275 | "threePointersAttempted": 6, 276 | "threePointersMade": 2, 277 | "threePointersPercentage": 0.333333333333333, 278 | "turnovers": 3, 279 | "twoPointersAttempted": 10, 280 | "twoPointersMade": 4, 281 | "twoPointersPercentage": 0.4 282 | }, 283 | "name": "Cade Cunningham", 284 | "nameI": "C. Cunningham", 285 | "firstName": "Cade", 286 | "familyName": "Cunningham" 287 | }, 288 | { 289 | "status": "ACTIVE", 290 | "order": 5, 291 | "personId": 1631093, 292 | "jerseyNum": "23", 293 | "position": "PG", 294 | "starter": "1", 295 | "oncourt": "0", 296 | "played": "1", 297 | "statistics": { 298 | "assists": 4, 299 | "blocks": 0, 300 | "blocksReceived": 0, 301 | "fieldGoalsAttempted": 15, 302 | "fieldGoalsMade": 8, 303 | "fieldGoalsPercentage": 0.533333333333333, 304 | "foulsOffensive": 0, 305 | "foulsDrawn": 5, 306 | "foulsPersonal": 3, 307 | "foulsTechnical": 0, 308 | "freeThrowsAttempted": 1, 309 | "freeThrowsMade": 1, 310 | "freeThrowsPercentage": 1.0, 311 | "minus": 81.0, 312 | "minutes": "PT31M43.95S", 313 | "minutesCalculated": "PT32M", 314 | "plus": 77.0, 315 | "plusMinusPoints": -4.0, 316 | "points": 19, 317 | "pointsFastBreak": 5, 318 | "pointsInThePaint": 12, 319 | "pointsSecondChance": 3, 320 | "reboundsDefensive": 2, 321 | "reboundsOffensive": 1, 322 | "reboundsTotal": 3, 323 | "steals": 3, 324 | "threePointersAttempted": 4, 325 | "threePointersMade": 2, 326 | "threePointersPercentage": 0.5, 327 | "turnovers": 2, 328 | "twoPointersAttempted": 11, 329 | "twoPointersMade": 6, 330 | "twoPointersPercentage": 0.545454545454545 331 | }, 332 | "name": "Jaden Ivey", 333 | "nameI": "J. Ivey", 334 | "firstName": "Jaden", 335 | "familyName": "Ivey" 336 | }, 337 | { 338 | "status": "ACTIVE", 339 | "order": 6, 340 | "personId": 1631105, 341 | "jerseyNum": "0", 342 | "starter": "0", 343 | "oncourt": "0", 344 | "played": "1", 345 | "statistics": { 346 | "assists": 1, 347 | "blocks": 3, 348 | "blocksReceived": 0, 349 | "fieldGoalsAttempted": 13, 350 | "fieldGoalsMade": 7, 351 | "fieldGoalsPercentage": 0.538461538461538, 352 | "foulsOffensive": 0, 353 | "foulsDrawn": 3, 354 | "foulsPersonal": 0, 355 | "foulsTechnical": 0, 356 | "freeThrowsAttempted": 4, 357 | "freeThrowsMade": 0, 358 | "freeThrowsPercentage": 0.0, 359 | "minus": 38.0, 360 | "minutes": "PT21M42.98S", 361 | "minutesCalculated": "PT22M", 362 | "plus": 45.0, 363 | "plusMinusPoints": 7.0, 364 | "points": 14, 365 | "pointsFastBreak": 4, 366 | "pointsInThePaint": 14, 367 | "pointsSecondChance": 4, 368 | "reboundsDefensive": 5, 369 | "reboundsOffensive": 5, 370 | "reboundsTotal": 10, 371 | "steals": 0, 372 | "threePointersAttempted": 0, 373 | "threePointersMade": 0, 374 | "threePointersPercentage": 0.0, 375 | "turnovers": 3, 376 | "twoPointersAttempted": 13, 377 | "twoPointersMade": 7, 378 | "twoPointersPercentage": 0.538461538461538 379 | }, 380 | "name": "Jalen Duren", 381 | "nameI": "J. Duren", 382 | "firstName": "Jalen", 383 | "familyName": "Duren" 384 | }, 385 | { 386 | "status": "ACTIVE", 387 | "order": 7, 388 | "personId": 1630165, 389 | "jerseyNum": "7", 390 | "starter": "0", 391 | "oncourt": "0", 392 | "played": "1", 393 | "statistics": { 394 | "assists": 5, 395 | "blocks": 1, 396 | "blocksReceived": 2, 397 | "fieldGoalsAttempted": 9, 398 | "fieldGoalsMade": 1, 399 | "fieldGoalsPercentage": 0.111111111111111, 400 | "foulsOffensive": 0, 401 | "foulsDrawn": 2, 402 | "foulsPersonal": 1, 403 | "foulsTechnical": 0, 404 | "freeThrowsAttempted": 1, 405 | "freeThrowsMade": 1, 406 | "freeThrowsPercentage": 1.0, 407 | "minus": 28.0, 408 | "minutes": "PT16M11.00S", 409 | "minutesCalculated": "PT16M", 410 | "plus": 34.0, 411 | "plusMinusPoints": 6.0, 412 | "points": 3, 413 | "pointsFastBreak": 2, 414 | "pointsInThePaint": 2, 415 | "pointsSecondChance": 0, 416 | "reboundsDefensive": 5, 417 | "reboundsOffensive": 0, 418 | "reboundsTotal": 5, 419 | "steals": 3, 420 | "threePointersAttempted": 3, 421 | "threePointersMade": 0, 422 | "threePointersPercentage": 0.0, 423 | "turnovers": 0, 424 | "twoPointersAttempted": 6, 425 | "twoPointersMade": 1, 426 | "twoPointersPercentage": 0.166666666666667 427 | }, 428 | "name": "Killian Hayes", 429 | "nameI": "K. Hayes", 430 | "firstName": "Killian", 431 | "familyName": "Hayes" 432 | }, 433 | { 434 | "status": "ACTIVE", 435 | "order": 8, 436 | "personId": 1628995, 437 | "jerseyNum": "20", 438 | "starter": "0", 439 | "oncourt": "0", 440 | "played": "1", 441 | "statistics": { 442 | "assists": 0, 443 | "blocks": 0, 444 | "blocksReceived": 1, 445 | "fieldGoalsAttempted": 8, 446 | "fieldGoalsMade": 1, 447 | "fieldGoalsPercentage": 0.125, 448 | "foulsOffensive": 0, 449 | "foulsDrawn": 0, 450 | "foulsPersonal": 2, 451 | "foulsTechnical": 0, 452 | "freeThrowsAttempted": 0, 453 | "freeThrowsMade": 0, 454 | "freeThrowsPercentage": 0.0, 455 | "minus": 24.0, 456 | "minutes": "PT13M10.00S", 457 | "minutesCalculated": "PT13M", 458 | "plus": 22.0, 459 | "plusMinusPoints": -2.0, 460 | "points": 3, 461 | "pointsFastBreak": 0, 462 | "pointsInThePaint": 0, 463 | "pointsSecondChance": 3, 464 | "reboundsDefensive": 3, 465 | "reboundsOffensive": 0, 466 | "reboundsTotal": 3, 467 | "steals": 0, 468 | "threePointersAttempted": 6, 469 | "threePointersMade": 1, 470 | "threePointersPercentage": 0.166666666666667, 471 | "turnovers": 2, 472 | "twoPointersAttempted": 2, 473 | "twoPointersMade": 0, 474 | "twoPointersPercentage": 0.0 475 | }, 476 | "name": "Kevin Knox II", 477 | "nameI": "K. Knox II", 478 | "firstName": "Kevin", 479 | "familyName": "Knox II" 480 | }, 481 | { 482 | "status": "ACTIVE", 483 | "order": 9, 484 | "personId": 202709, 485 | "jerseyNum": "18", 486 | "starter": "0", 487 | "oncourt": "1", 488 | "played": "1", 489 | "statistics": { 490 | "assists": 2, 491 | "blocks": 0, 492 | "blocksReceived": 0, 493 | "fieldGoalsAttempted": 3, 494 | "fieldGoalsMade": 3, 495 | "fieldGoalsPercentage": 1.0, 496 | "foulsOffensive": 0, 497 | "foulsDrawn": 1, 498 | "foulsPersonal": 3, 499 | "foulsTechnical": 0, 500 | "freeThrowsAttempted": 0, 501 | "freeThrowsMade": 0, 502 | "freeThrowsPercentage": 0.0, 503 | "minus": 30.0, 504 | "minutes": "PT15M07.07S", 505 | "minutesCalculated": "PT15M", 506 | "plus": 29.0, 507 | "plusMinusPoints": -1.0, 508 | "points": 8, 509 | "pointsFastBreak": 0, 510 | "pointsInThePaint": 0, 511 | "pointsSecondChance": 0, 512 | "reboundsDefensive": 1, 513 | "reboundsOffensive": 0, 514 | "reboundsTotal": 1, 515 | "steals": 0, 516 | "threePointersAttempted": 2, 517 | "threePointersMade": 2, 518 | "threePointersPercentage": 1.0, 519 | "turnovers": 0, 520 | "twoPointersAttempted": 1, 521 | "twoPointersMade": 1, 522 | "twoPointersPercentage": 1.0 523 | }, 524 | "name": "Cory Joseph", 525 | "nameI": "C. Joseph", 526 | "firstName": "Cory", 527 | "familyName": "Joseph" 528 | }, 529 | { 530 | "status": "ACTIVE", 531 | "order": 10, 532 | "personId": 1628977, 533 | "jerseyNum": "6", 534 | "starter": "0", 535 | "oncourt": "0", 536 | "played": "1", 537 | "statistics": { 538 | "assists": 1, 539 | "blocks": 0, 540 | "blocksReceived": 0, 541 | "fieldGoalsAttempted": 3, 542 | "fieldGoalsMade": 1, 543 | "fieldGoalsPercentage": 0.333333333333333, 544 | "foulsOffensive": 0, 545 | "foulsDrawn": 0, 546 | "foulsPersonal": 3, 547 | "foulsTechnical": 0, 548 | "freeThrowsAttempted": 0, 549 | "freeThrowsMade": 0, 550 | "freeThrowsPercentage": 0.0, 551 | "minus": 30.0, 552 | "minutes": "PT14M28.00S", 553 | "minutesCalculated": "PT15M", 554 | "plus": 30.0, 555 | "plusMinusPoints": 0.0, 556 | "points": 2, 557 | "pointsFastBreak": 0, 558 | "pointsInThePaint": 2, 559 | "pointsSecondChance": 0, 560 | "reboundsDefensive": 0, 561 | "reboundsOffensive": 2, 562 | "reboundsTotal": 2, 563 | "steals": 0, 564 | "threePointersAttempted": 1, 565 | "threePointersMade": 0, 566 | "threePointersPercentage": 0.0, 567 | "turnovers": 0, 568 | "twoPointersAttempted": 2, 569 | "twoPointersMade": 1, 570 | "twoPointersPercentage": 0.5 571 | }, 572 | "name": "Hamidou Diallo", 573 | "nameI": "H. Diallo", 574 | "firstName": "Hamidou", 575 | "familyName": "Diallo" 576 | }, 577 | { 578 | "status": "ACTIVE", 579 | "order": 11, 580 | "personId": 1631205, 581 | "jerseyNum": "27", 582 | "starter": "0", 583 | "oncourt": "0", 584 | "played": "0", 585 | "statistics": { 586 | "assists": 0, 587 | "blocks": 0, 588 | "blocksReceived": 0, 589 | "fieldGoalsAttempted": 0, 590 | "fieldGoalsMade": 0, 591 | "fieldGoalsPercentage": 0.0, 592 | "foulsOffensive": 0, 593 | "foulsDrawn": 0, 594 | "foulsPersonal": 0, 595 | "foulsTechnical": 0, 596 | "freeThrowsAttempted": 0, 597 | "freeThrowsMade": 0, 598 | "freeThrowsPercentage": 0.0, 599 | "minus": 0.0, 600 | "minutes": "PT00M00.00S", 601 | "minutesCalculated": "PT00M", 602 | "plus": 0.0, 603 | "plusMinusPoints": 0.0, 604 | "points": 0, 605 | "pointsFastBreak": 0, 606 | "pointsInThePaint": 0, 607 | "pointsSecondChance": 0, 608 | "reboundsDefensive": 0, 609 | "reboundsOffensive": 0, 610 | "reboundsTotal": 0, 611 | "steals": 0, 612 | "threePointersAttempted": 0, 613 | "threePointersMade": 0, 614 | "threePointersPercentage": 0.0, 615 | "turnovers": 0, 616 | "twoPointersAttempted": 0, 617 | "twoPointersMade": 0, 618 | "twoPointersPercentage": 0.0 619 | }, 620 | "name": "Buddy Boeheim", 621 | "nameI": "B. Boeheim", 622 | "firstName": "Buddy", 623 | "familyName": "Boeheim" 624 | }, 625 | { 626 | "status": "ACTIVE", 627 | "order": 12, 628 | "personId": 1630296, 629 | "jerseyNum": "8", 630 | "starter": "0", 631 | "oncourt": "0", 632 | "played": "0", 633 | "statistics": { 634 | "assists": 0, 635 | "blocks": 0, 636 | "blocksReceived": 0, 637 | "fieldGoalsAttempted": 0, 638 | "fieldGoalsMade": 0, 639 | "fieldGoalsPercentage": 0.0, 640 | "foulsOffensive": 0, 641 | "foulsDrawn": 0, 642 | "foulsPersonal": 0, 643 | "foulsTechnical": 0, 644 | "freeThrowsAttempted": 0, 645 | "freeThrowsMade": 0, 646 | "freeThrowsPercentage": 0.0, 647 | "minus": 0.0, 648 | "minutes": "PT00M00.00S", 649 | "minutesCalculated": "PT00M", 650 | "plus": 0.0, 651 | "plusMinusPoints": 0.0, 652 | "points": 0, 653 | "pointsFastBreak": 0, 654 | "pointsInThePaint": 0, 655 | "pointsSecondChance": 0, 656 | "reboundsDefensive": 0, 657 | "reboundsOffensive": 0, 658 | "reboundsTotal": 0, 659 | "steals": 0, 660 | "threePointersAttempted": 0, 661 | "threePointersMade": 0, 662 | "threePointersPercentage": 0.0, 663 | "turnovers": 0, 664 | "twoPointersAttempted": 0, 665 | "twoPointersMade": 0, 666 | "twoPointersPercentage": 0.0 667 | }, 668 | "name": "Braxton Key", 669 | "nameI": "B. Key", 670 | "firstName": "Braxton", 671 | "familyName": "Key" 672 | }, 673 | { 674 | "status": "ACTIVE", 675 | "order": 13, 676 | "personId": 203585, 677 | "jerseyNum": "17", 678 | "starter": "0", 679 | "oncourt": "0", 680 | "played": "0", 681 | "statistics": { 682 | "assists": 0, 683 | "blocks": 0, 684 | "blocksReceived": 0, 685 | "fieldGoalsAttempted": 0, 686 | "fieldGoalsMade": 0, 687 | "fieldGoalsPercentage": 0.0, 688 | "foulsOffensive": 0, 689 | "foulsDrawn": 0, 690 | "foulsPersonal": 0, 691 | "foulsTechnical": 0, 692 | "freeThrowsAttempted": 0, 693 | "freeThrowsMade": 0, 694 | "freeThrowsPercentage": 0.0, 695 | "minus": 0.0, 696 | "minutes": "PT00M00.00S", 697 | "minutesCalculated": "PT00M", 698 | "plus": 0.0, 699 | "plusMinusPoints": 0.0, 700 | "points": 0, 701 | "pointsFastBreak": 0, 702 | "pointsInThePaint": 0, 703 | "pointsSecondChance": 0, 704 | "reboundsDefensive": 0, 705 | "reboundsOffensive": 0, 706 | "reboundsTotal": 0, 707 | "steals": 0, 708 | "threePointersAttempted": 0, 709 | "threePointersMade": 0, 710 | "threePointersPercentage": 0.0, 711 | "turnovers": 0, 712 | "twoPointersAttempted": 0, 713 | "twoPointersMade": 0, 714 | "twoPointersPercentage": 0.0 715 | }, 716 | "name": "Rodney McGruder", 717 | "nameI": "R. McGruder", 718 | "firstName": "Rodney", 719 | "familyName": "McGruder" 720 | }, 721 | { 722 | "status": "ACTIVE", 723 | "order": 14, 724 | "personId": 203457, 725 | "jerseyNum": "9", 726 | "starter": "0", 727 | "oncourt": "0", 728 | "played": "0", 729 | "statistics": { 730 | "assists": 0, 731 | "blocks": 0, 732 | "blocksReceived": 0, 733 | "fieldGoalsAttempted": 0, 734 | "fieldGoalsMade": 0, 735 | "fieldGoalsPercentage": 0.0, 736 | "foulsOffensive": 0, 737 | "foulsDrawn": 0, 738 | "foulsPersonal": 0, 739 | "foulsTechnical": 0, 740 | "freeThrowsAttempted": 0, 741 | "freeThrowsMade": 0, 742 | "freeThrowsPercentage": 0.0, 743 | "minus": 0.0, 744 | "minutes": "PT00M00.00S", 745 | "minutesCalculated": "PT00M", 746 | "plus": 0.0, 747 | "plusMinusPoints": 0.0, 748 | "points": 0, 749 | "pointsFastBreak": 0, 750 | "pointsInThePaint": 0, 751 | "pointsSecondChance": 0, 752 | "reboundsDefensive": 0, 753 | "reboundsOffensive": 0, 754 | "reboundsTotal": 0, 755 | "steals": 0, 756 | "threePointersAttempted": 0, 757 | "threePointersMade": 0, 758 | "threePointersPercentage": 0.0, 759 | "turnovers": 0, 760 | "twoPointersAttempted": 0, 761 | "twoPointersMade": 0, 762 | "twoPointersPercentage": 0.0 763 | }, 764 | "name": "Nerlens Noel", 765 | "nameI": "N. Noel", 766 | "firstName": "Nerlens", 767 | "familyName": "Noel" 768 | }, 769 | { 770 | "status": "INACTIVE", 771 | "notPlayingReason": "INACTIVE_INJURY", 772 | "notPlayingDescription": "Right Knee; Sprain", 773 | "order": 15, 774 | "personId": 1628963, 775 | "jerseyNum": "35", 776 | "starter": "0", 777 | "oncourt": "0", 778 | "played": "0", 779 | "statistics": { 780 | "assists": 0, 781 | "blocks": 0, 782 | "blocksReceived": 0, 783 | "fieldGoalsAttempted": 0, 784 | "fieldGoalsMade": 0, 785 | "fieldGoalsPercentage": 0.0, 786 | "foulsOffensive": 0, 787 | "foulsDrawn": 0, 788 | "foulsPersonal": 0, 789 | "foulsTechnical": 0, 790 | "freeThrowsAttempted": 0, 791 | "freeThrowsMade": 0, 792 | "freeThrowsPercentage": 0.0, 793 | "minus": 0.0, 794 | "minutes": "PT00M00.00S", 795 | "minutesCalculated": "PT00M", 796 | "plus": 0.0, 797 | "plusMinusPoints": 0.0, 798 | "points": 0, 799 | "pointsFastBreak": 0, 800 | "pointsInThePaint": 0, 801 | "pointsSecondChance": 0, 802 | "reboundsDefensive": 0, 803 | "reboundsOffensive": 0, 804 | "reboundsTotal": 0, 805 | "steals": 0, 806 | "threePointersAttempted": 0, 807 | "threePointersMade": 0, 808 | "threePointersPercentage": 0.0, 809 | "turnovers": 0, 810 | "twoPointersAttempted": 0, 811 | "twoPointersMade": 0, 812 | "twoPointersPercentage": 0.0 813 | }, 814 | "name": "Marvin Bagley III", 815 | "nameI": "M. Bagley III", 816 | "firstName": "Marvin", 817 | "familyName": "Bagley III" 818 | }, 819 | { 820 | "status": "INACTIVE", 821 | "notPlayingReason": "INACTIVE_INJURY", 822 | "notPlayingDescription": "Left Navicular; Fracture", 823 | "order": 16, 824 | "personId": 202692, 825 | "jerseyNum": "5", 826 | "starter": "0", 827 | "oncourt": "0", 828 | "played": "0", 829 | "statistics": { 830 | "assists": 0, 831 | "blocks": 0, 832 | "blocksReceived": 0, 833 | "fieldGoalsAttempted": 0, 834 | "fieldGoalsMade": 0, 835 | "fieldGoalsPercentage": 0.0, 836 | "foulsOffensive": 0, 837 | "foulsDrawn": 0, 838 | "foulsPersonal": 0, 839 | "foulsTechnical": 0, 840 | "freeThrowsAttempted": 0, 841 | "freeThrowsMade": 0, 842 | "freeThrowsPercentage": 0.0, 843 | "minus": 0.0, 844 | "minutes": "PT00M00.00S", 845 | "minutesCalculated": "PT00M", 846 | "plus": 0.0, 847 | "plusMinusPoints": 0.0, 848 | "points": 0, 849 | "pointsFastBreak": 0, 850 | "pointsInThePaint": 0, 851 | "pointsSecondChance": 0, 852 | "reboundsDefensive": 0, 853 | "reboundsOffensive": 0, 854 | "reboundsTotal": 0, 855 | "steals": 0, 856 | "threePointersAttempted": 0, 857 | "threePointersMade": 0, 858 | "threePointersPercentage": 0.0, 859 | "turnovers": 0, 860 | "twoPointersAttempted": 0, 861 | "twoPointersMade": 0, 862 | "twoPointersPercentage": 0.0 863 | }, 864 | "name": "Alec Burks", 865 | "nameI": "A. Burks", 866 | "firstName": "Alec", 867 | "familyName": "Burks" 868 | }, 869 | { 870 | "status": "INACTIVE", 871 | "notPlayingReason": "INACTIVE_INJURY", 872 | "notPlayingDescription": "Right Hip; Soreness", 873 | "order": 17, 874 | "personId": 1630587, 875 | "jerseyNum": "12", 876 | "starter": "0", 877 | "oncourt": "0", 878 | "played": "0", 879 | "statistics": { 880 | "assists": 0, 881 | "blocks": 0, 882 | "blocksReceived": 0, 883 | "fieldGoalsAttempted": 0, 884 | "fieldGoalsMade": 0, 885 | "fieldGoalsPercentage": 0.0, 886 | "foulsOffensive": 0, 887 | "foulsDrawn": 0, 888 | "foulsPersonal": 0, 889 | "foulsTechnical": 0, 890 | "freeThrowsAttempted": 0, 891 | "freeThrowsMade": 0, 892 | "freeThrowsPercentage": 0.0, 893 | "minus": 0.0, 894 | "minutes": "PT00M00.00S", 895 | "minutesCalculated": "PT00M", 896 | "plus": 0.0, 897 | "plusMinusPoints": 0.0, 898 | "points": 0, 899 | "pointsFastBreak": 0, 900 | "pointsInThePaint": 0, 901 | "pointsSecondChance": 0, 902 | "reboundsDefensive": 0, 903 | "reboundsOffensive": 0, 904 | "reboundsTotal": 0, 905 | "steals": 0, 906 | "threePointersAttempted": 0, 907 | "threePointersMade": 0, 908 | "threePointersPercentage": 0.0, 909 | "turnovers": 0, 910 | "twoPointersAttempted": 0, 911 | "twoPointersMade": 0, 912 | "twoPointersPercentage": 0.0 913 | }, 914 | "name": "Isaiah Livers", 915 | "nameI": "I. Livers", 916 | "firstName": "Isaiah", 917 | "familyName": "Livers" 918 | } 919 | ], 920 | "statistics": { 921 | "assists": 31, 922 | "assistsTurnoverRatio": 2.38461538461538, 923 | "benchPoints": 30, 924 | "biggestLead": 12, 925 | "biggestLeadScore": "72-84", 926 | "biggestScoringRun": 19, 927 | "biggestScoringRunScore": "21-6", 928 | "blocks": 4, 929 | "blocksReceived": 5, 930 | "fastBreakPointsAttempted": 14, 931 | "fastBreakPointsMade": 8, 932 | "fastBreakPointsPercentage": 0.5714285714285711, 933 | "fieldGoalsAttempted": 94, 934 | "fieldGoalsEffectiveAdjusted": 0.5, 935 | "fieldGoalsMade": 40, 936 | "fieldGoalsPercentage": 0.425531914893617, 937 | "foulsOffensive": 0, 938 | "foulsDrawn": 24, 939 | "foulsPersonal": 21, 940 | "foulsTeam": 21, 941 | "foulsTechnical": 0, 942 | "foulsTeamTechnical": 0, 943 | "freeThrowsAttempted": 24, 944 | "freeThrowsMade": 19, 945 | "freeThrowsPercentage": 0.791666666666666, 946 | "leadChanges": 7, 947 | "minutes": "PT240M00.00S", 948 | "minutesCalculated": "PT240M", 949 | "points": 113, 950 | "pointsAgainst": 109, 951 | "pointsFastBreak": 18, 952 | "pointsFromTurnovers": 24, 953 | "pointsInThePaint": 48, 954 | "pointsInThePaintAttempted": 51, 955 | "pointsInThePaintMade": 24, 956 | "pointsInThePaintPercentage": 0.470588235294118, 957 | "pointsSecondChance": 16, 958 | "reboundsDefensive": 29, 959 | "reboundsOffensive": 12, 960 | "reboundsPersonal": 41, 961 | "reboundsTeam": 11, 962 | "reboundsTeamDefensive": 5, 963 | "reboundsTeamOffensive": 6, 964 | "reboundsTotal": 52, 965 | "secondChancePointsAttempted": 13, 966 | "secondChancePointsMade": 6, 967 | "secondChancePointsPercentage": 0.461538461538462, 968 | "steals": 11, 969 | "threePointersAttempted": 38, 970 | "threePointersMade": 14, 971 | "threePointersPercentage": 0.368421052631579, 972 | "timeLeading": "PT26M55.00S", 973 | "timesTied": 4, 974 | "trueShootingAttempts": 104.56, 975 | "trueShootingPercentage": 0.540359602142311, 976 | "turnovers": 12, 977 | "turnoversTeam": 1, 978 | "turnoversTotal": 13, 979 | "twoPointersAttempted": 56, 980 | "twoPointersMade": 26, 981 | "twoPointersPercentage": 0.464285714285714 982 | } 983 | }, 984 | "awayTeam": { 985 | "teamId": 1610612753, 986 | "teamName": "Magic", 987 | "teamCity": "Orlando", 988 | "teamTricode": "ORL", 989 | "score": 109, 990 | "inBonus": "1", 991 | "timeoutsRemaining": 0, 992 | "periods": [ 993 | { 994 | "period": 1, 995 | "periodType": "REGULAR", 996 | "score": 28 997 | }, 998 | { 999 | "period": 2, 1000 | "periodType": "REGULAR", 1001 | "score": 27 1002 | }, 1003 | { 1004 | "period": 3, 1005 | "periodType": "REGULAR", 1006 | "score": 28 1007 | }, 1008 | { 1009 | "period": 4, 1010 | "periodType": "REGULAR", 1011 | "score": 26 1012 | } 1013 | ], 1014 | "players": [ 1015 | { 1016 | "status": "ACTIVE", 1017 | "order": 1, 1018 | "personId": 1631094, 1019 | "jerseyNum": "5", 1020 | "position": "SF", 1021 | "starter": "1", 1022 | "oncourt": "1", 1023 | "played": "1", 1024 | "statistics": { 1025 | "assists": 5, 1026 | "blocks": 2, 1027 | "blocksReceived": 1, 1028 | "fieldGoalsAttempted": 18, 1029 | "fieldGoalsMade": 11, 1030 | "fieldGoalsPercentage": 0.611111111111111, 1031 | "foulsOffensive": 0, 1032 | "foulsDrawn": 5, 1033 | "foulsPersonal": 5, 1034 | "foulsTechnical": 0, 1035 | "freeThrowsAttempted": 7, 1036 | "freeThrowsMade": 5, 1037 | "freeThrowsPercentage": 0.714285714285714, 1038 | "minus": 78.0, 1039 | "minutes": "PT35M20.00S", 1040 | "minutesCalculated": "PT35M", 1041 | "plus": 80.0, 1042 | "plusMinusPoints": 2.0, 1043 | "points": 27, 1044 | "pointsFastBreak": 6, 1045 | "pointsInThePaint": 20, 1046 | "pointsSecondChance": 4, 1047 | "reboundsDefensive": 7, 1048 | "reboundsOffensive": 2, 1049 | "reboundsTotal": 9, 1050 | "steals": 0, 1051 | "threePointersAttempted": 0, 1052 | "threePointersMade": 0, 1053 | "threePointersPercentage": 0.0, 1054 | "turnovers": 4, 1055 | "twoPointersAttempted": 18, 1056 | "twoPointersMade": 11, 1057 | "twoPointersPercentage": 0.611111111111111 1058 | }, 1059 | "name": "Paolo Banchero", 1060 | "nameI": "P. Banchero", 1061 | "firstName": "Paolo", 1062 | "familyName": "Banchero" 1063 | }, 1064 | { 1065 | "status": "ACTIVE", 1066 | "order": 2, 1067 | "personId": 1630532, 1068 | "jerseyNum": "22", 1069 | "position": "PF", 1070 | "starter": "1", 1071 | "oncourt": "1", 1072 | "played": "1", 1073 | "statistics": { 1074 | "assists": 5, 1075 | "blocks": 0, 1076 | "blocksReceived": 1, 1077 | "fieldGoalsAttempted": 18, 1078 | "fieldGoalsMade": 8, 1079 | "fieldGoalsPercentage": 0.444444444444444, 1080 | "foulsOffensive": 0, 1081 | "foulsDrawn": 2, 1082 | "foulsPersonal": 2, 1083 | "foulsTechnical": 0, 1084 | "freeThrowsAttempted": 2, 1085 | "freeThrowsMade": 2, 1086 | "freeThrowsPercentage": 1.0, 1087 | "minus": 80.0, 1088 | "minutes": "PT34M07.00S", 1089 | "minutesCalculated": "PT34M", 1090 | "plus": 78.0, 1091 | "plusMinusPoints": -2.0, 1092 | "points": 20, 1093 | "pointsFastBreak": 4, 1094 | "pointsInThePaint": 10, 1095 | "pointsSecondChance": 2, 1096 | "reboundsDefensive": 3, 1097 | "reboundsOffensive": 1, 1098 | "reboundsTotal": 4, 1099 | "steals": 0, 1100 | "threePointersAttempted": 6, 1101 | "threePointersMade": 2, 1102 | "threePointersPercentage": 0.333333333333333, 1103 | "turnovers": 5, 1104 | "twoPointersAttempted": 12, 1105 | "twoPointersMade": 6, 1106 | "twoPointersPercentage": 0.5 1107 | }, 1108 | "name": "Franz Wagner", 1109 | "nameI": "F. Wagner", 1110 | "firstName": "Franz", 1111 | "familyName": "Wagner" 1112 | }, 1113 | { 1114 | "status": "ACTIVE", 1115 | "order": 3, 1116 | "personId": 1628976, 1117 | "jerseyNum": "34", 1118 | "position": "C", 1119 | "starter": "1", 1120 | "oncourt": "1", 1121 | "played": "1", 1122 | "statistics": { 1123 | "assists": 3, 1124 | "blocks": 1, 1125 | "blocksReceived": 0, 1126 | "fieldGoalsAttempted": 8, 1127 | "fieldGoalsMade": 5, 1128 | "fieldGoalsPercentage": 0.625, 1129 | "foulsOffensive": 0, 1130 | "foulsDrawn": 4, 1131 | "foulsPersonal": 4, 1132 | "foulsTechnical": 0, 1133 | "freeThrowsAttempted": 0, 1134 | "freeThrowsMade": 0, 1135 | "freeThrowsPercentage": 0.0, 1136 | "minus": 76.0, 1137 | "minutes": "PT32M39.00S", 1138 | "minutesCalculated": "PT33M", 1139 | "plus": 76.0, 1140 | "plusMinusPoints": 0.0, 1141 | "points": 11, 1142 | "pointsFastBreak": 3, 1143 | "pointsInThePaint": 8, 1144 | "pointsSecondChance": 2, 1145 | "reboundsDefensive": 11, 1146 | "reboundsOffensive": 0, 1147 | "reboundsTotal": 11, 1148 | "steals": 1, 1149 | "threePointersAttempted": 2, 1150 | "threePointersMade": 1, 1151 | "threePointersPercentage": 0.5, 1152 | "turnovers": 3, 1153 | "twoPointersAttempted": 6, 1154 | "twoPointersMade": 4, 1155 | "twoPointersPercentage": 0.666666666666666 1156 | }, 1157 | "name": "Wendell Carter Jr.", 1158 | "nameI": "W. Carter Jr.", 1159 | "firstName": "Wendell", 1160 | "familyName": "Carter Jr." 1161 | }, 1162 | { 1163 | "status": "ACTIVE", 1164 | "order": 4, 1165 | "personId": 203082, 1166 | "jerseyNum": "31", 1167 | "position": "SG", 1168 | "starter": "1", 1169 | "oncourt": "1", 1170 | "played": "1", 1171 | "statistics": { 1172 | "assists": 1, 1173 | "blocks": 0, 1174 | "blocksReceived": 0, 1175 | "fieldGoalsAttempted": 12, 1176 | "fieldGoalsMade": 5, 1177 | "fieldGoalsPercentage": 0.416666666666667, 1178 | "foulsOffensive": 0, 1179 | "foulsDrawn": 2, 1180 | "foulsPersonal": 2, 1181 | "foulsTechnical": 0, 1182 | "freeThrowsAttempted": 1, 1183 | "freeThrowsMade": 0, 1184 | "freeThrowsPercentage": 0.0, 1185 | "minus": 76.0, 1186 | "minutes": "PT31M49.01S", 1187 | "minutesCalculated": "PT32M", 1188 | "plus": 81.0, 1189 | "plusMinusPoints": 5.0, 1190 | "points": 13, 1191 | "pointsFastBreak": 3, 1192 | "pointsInThePaint": 2, 1193 | "pointsSecondChance": 0, 1194 | "reboundsDefensive": 3, 1195 | "reboundsOffensive": 1, 1196 | "reboundsTotal": 4, 1197 | "steals": 1, 1198 | "threePointersAttempted": 8, 1199 | "threePointersMade": 3, 1200 | "threePointersPercentage": 0.375, 1201 | "turnovers": 0, 1202 | "twoPointersAttempted": 4, 1203 | "twoPointersMade": 2, 1204 | "twoPointersPercentage": 0.5 1205 | }, 1206 | "name": "Terrence Ross", 1207 | "nameI": "T. Ross", 1208 | "firstName": "Terrence", 1209 | "familyName": "Ross" 1210 | }, 1211 | { 1212 | "status": "ACTIVE", 1213 | "order": 5, 1214 | "personId": 1630591, 1215 | "jerseyNum": "4", 1216 | "position": "PG", 1217 | "starter": "1", 1218 | "oncourt": "0", 1219 | "played": "1", 1220 | "statistics": { 1221 | "assists": 3, 1222 | "blocks": 0, 1223 | "blocksReceived": 0, 1224 | "fieldGoalsAttempted": 11, 1225 | "fieldGoalsMade": 8, 1226 | "fieldGoalsPercentage": 0.7272727272727271, 1227 | "foulsOffensive": 1, 1228 | "foulsDrawn": 2, 1229 | "foulsPersonal": 6, 1230 | "foulsTechnical": 0, 1231 | "freeThrowsAttempted": 1, 1232 | "freeThrowsMade": 1, 1233 | "freeThrowsPercentage": 1.0, 1234 | "minus": 58.0, 1235 | "minutes": "PT25M14.00S", 1236 | "minutesCalculated": "PT25M", 1237 | "plus": 61.0, 1238 | "plusMinusPoints": 3.0, 1239 | "points": 21, 1240 | "pointsFastBreak": 4, 1241 | "pointsInThePaint": 8, 1242 | "pointsSecondChance": 2, 1243 | "reboundsDefensive": 0, 1244 | "reboundsOffensive": 1, 1245 | "reboundsTotal": 1, 1246 | "steals": 2, 1247 | "threePointersAttempted": 6, 1248 | "threePointersMade": 4, 1249 | "threePointersPercentage": 0.666666666666666, 1250 | "turnovers": 4, 1251 | "twoPointersAttempted": 5, 1252 | "twoPointersMade": 4, 1253 | "twoPointersPercentage": 0.8 1254 | }, 1255 | "name": "Jalen Suggs", 1256 | "nameI": "J. Suggs", 1257 | "firstName": "Jalen", 1258 | "familyName": "Suggs" 1259 | }, 1260 | { 1261 | "status": "ACTIVE", 1262 | "order": 6, 1263 | "personId": 1631216, 1264 | "jerseyNum": "2", 1265 | "starter": "0", 1266 | "oncourt": "0", 1267 | "played": "1", 1268 | "statistics": { 1269 | "assists": 0, 1270 | "blocks": 1, 1271 | "blocksReceived": 0, 1272 | "fieldGoalsAttempted": 3, 1273 | "fieldGoalsMade": 0, 1274 | "fieldGoalsPercentage": 0.0, 1275 | "foulsOffensive": 0, 1276 | "foulsDrawn": 0, 1277 | "foulsPersonal": 3, 1278 | "foulsTechnical": 0, 1279 | "freeThrowsAttempted": 0, 1280 | "freeThrowsMade": 0, 1281 | "freeThrowsPercentage": 0.0, 1282 | "minus": 60.0, 1283 | "minutes": "PT21M55.00S", 1284 | "minutesCalculated": "PT22M", 1285 | "plus": 46.0, 1286 | "plusMinusPoints": -14.0, 1287 | "points": 0, 1288 | "pointsFastBreak": 0, 1289 | "pointsInThePaint": 0, 1290 | "pointsSecondChance": 0, 1291 | "reboundsDefensive": 3, 1292 | "reboundsOffensive": 0, 1293 | "reboundsTotal": 3, 1294 | "steals": 0, 1295 | "threePointersAttempted": 2, 1296 | "threePointersMade": 0, 1297 | "threePointersPercentage": 0.0, 1298 | "turnovers": 0, 1299 | "twoPointersAttempted": 1, 1300 | "twoPointersMade": 0, 1301 | "twoPointersPercentage": 0.0 1302 | }, 1303 | "name": "Caleb Houstan", 1304 | "nameI": "C. Houstan", 1305 | "firstName": "Caleb", 1306 | "familyName": "Houstan" 1307 | }, 1308 | { 1309 | "status": "ACTIVE", 1310 | "order": 7, 1311 | "personId": 1629626, 1312 | "jerseyNum": "10", 1313 | "starter": "0", 1314 | "oncourt": "0", 1315 | "played": "1", 1316 | "statistics": { 1317 | "assists": 0, 1318 | "blocks": 1, 1319 | "blocksReceived": 0, 1320 | "fieldGoalsAttempted": 6, 1321 | "fieldGoalsMade": 4, 1322 | "fieldGoalsPercentage": 0.666666666666666, 1323 | "foulsOffensive": 0, 1324 | "foulsDrawn": 1, 1325 | "foulsPersonal": 0, 1326 | "foulsTechnical": 0, 1327 | "freeThrowsAttempted": 2, 1328 | "freeThrowsMade": 2, 1329 | "freeThrowsPercentage": 1.0, 1330 | "minus": 36.0, 1331 | "minutes": "PT17M32.00S", 1332 | "minutesCalculated": "PT18M", 1333 | "plus": 32.0, 1334 | "plusMinusPoints": -4.0, 1335 | "points": 10, 1336 | "pointsFastBreak": 0, 1337 | "pointsInThePaint": 8, 1338 | "pointsSecondChance": 4, 1339 | "reboundsDefensive": 4, 1340 | "reboundsOffensive": 2, 1341 | "reboundsTotal": 6, 1342 | "steals": 0, 1343 | "threePointersAttempted": 0, 1344 | "threePointersMade": 0, 1345 | "threePointersPercentage": 0.0, 1346 | "turnovers": 2, 1347 | "twoPointersAttempted": 6, 1348 | "twoPointersMade": 4, 1349 | "twoPointersPercentage": 0.666666666666666 1350 | }, 1351 | "name": "Bol Bol", 1352 | "nameI": "B. Bol", 1353 | "firstName": "Bol", 1354 | "familyName": "Bol" 1355 | }, 1356 | { 1357 | "status": "ACTIVE", 1358 | "order": 8, 1359 | "personId": 1628964, 1360 | "jerseyNum": "11", 1361 | "starter": "0", 1362 | "oncourt": "0", 1363 | "played": "1", 1364 | "statistics": { 1365 | "assists": 1, 1366 | "blocks": 0, 1367 | "blocksReceived": 1, 1368 | "fieldGoalsAttempted": 5, 1369 | "fieldGoalsMade": 0, 1370 | "fieldGoalsPercentage": 0.0, 1371 | "foulsOffensive": 0, 1372 | "foulsDrawn": 2, 1373 | "foulsPersonal": 0, 1374 | "foulsTechnical": 0, 1375 | "freeThrowsAttempted": 2, 1376 | "freeThrowsMade": 0, 1377 | "freeThrowsPercentage": 0.0, 1378 | "minus": 35.0, 1379 | "minutes": "PT12M00.00S", 1380 | "minutesCalculated": "PT12M", 1381 | "plus": 25.0, 1382 | "plusMinusPoints": -10.0, 1383 | "points": 0, 1384 | "pointsFastBreak": 0, 1385 | "pointsInThePaint": 0, 1386 | "pointsSecondChance": 0, 1387 | "reboundsDefensive": 1, 1388 | "reboundsOffensive": 1, 1389 | "reboundsTotal": 2, 1390 | "steals": 0, 1391 | "threePointersAttempted": 4, 1392 | "threePointersMade": 0, 1393 | "threePointersPercentage": 0.0, 1394 | "turnovers": 0, 1395 | "twoPointersAttempted": 1, 1396 | "twoPointersMade": 0, 1397 | "twoPointersPercentage": 0.0 1398 | }, 1399 | "name": "Mo Bamba", 1400 | "nameI": "M. Bamba", 1401 | "firstName": "Mo", 1402 | "familyName": "Bamba" 1403 | }, 1404 | { 1405 | "status": "ACTIVE", 1406 | "order": 9, 1407 | "personId": 1629643, 1408 | "jerseyNum": "3", 1409 | "starter": "0", 1410 | "oncourt": "0", 1411 | "played": "1", 1412 | "statistics": { 1413 | "assists": 1, 1414 | "blocks": 0, 1415 | "blocksReceived": 1, 1416 | "fieldGoalsAttempted": 3, 1417 | "fieldGoalsMade": 0, 1418 | "fieldGoalsPercentage": 0.0, 1419 | "foulsOffensive": 0, 1420 | "foulsDrawn": 1, 1421 | "foulsPersonal": 2, 1422 | "foulsTechnical": 0, 1423 | "freeThrowsAttempted": 2, 1424 | "freeThrowsMade": 2, 1425 | "freeThrowsPercentage": 1.0, 1426 | "minus": 47.0, 1427 | "minutes": "PT17M54.99S", 1428 | "minutesCalculated": "PT18M", 1429 | "plus": 40.0, 1430 | "plusMinusPoints": -7.0, 1431 | "points": 2, 1432 | "pointsFastBreak": 0, 1433 | "pointsInThePaint": 0, 1434 | "pointsSecondChance": 2, 1435 | "reboundsDefensive": 3, 1436 | "reboundsOffensive": 1, 1437 | "reboundsTotal": 4, 1438 | "steals": 0, 1439 | "threePointersAttempted": 1, 1440 | "threePointersMade": 0, 1441 | "threePointersPercentage": 0.0, 1442 | "turnovers": 0, 1443 | "twoPointersAttempted": 2, 1444 | "twoPointersMade": 0, 1445 | "twoPointersPercentage": 0.0 1446 | }, 1447 | "name": "Chuma Okeke", 1448 | "nameI": "C. Okeke", 1449 | "firstName": "Chuma", 1450 | "familyName": "Okeke" 1451 | }, 1452 | { 1453 | "status": "ACTIVE", 1454 | "order": 10, 1455 | "personId": 1630181, 1456 | "jerseyNum": "13", 1457 | "starter": "0", 1458 | "oncourt": "1", 1459 | "played": "1", 1460 | "statistics": { 1461 | "assists": 2, 1462 | "blocks": 0, 1463 | "blocksReceived": 0, 1464 | "fieldGoalsAttempted": 2, 1465 | "fieldGoalsMade": 1, 1466 | "fieldGoalsPercentage": 0.5, 1467 | "foulsOffensive": 0, 1468 | "foulsDrawn": 2, 1469 | "foulsPersonal": 0, 1470 | "foulsTechnical": 0, 1471 | "freeThrowsAttempted": 2, 1472 | "freeThrowsMade": 2, 1473 | "freeThrowsPercentage": 1.0, 1474 | "minus": 19.0, 1475 | "minutes": "PT11M29.00S", 1476 | "minutesCalculated": "PT11M", 1477 | "plus": 26.0, 1478 | "plusMinusPoints": 7.0, 1479 | "points": 5, 1480 | "pointsFastBreak": 3, 1481 | "pointsInThePaint": 0, 1482 | "pointsSecondChance": 0, 1483 | "reboundsDefensive": 3, 1484 | "reboundsOffensive": 1, 1485 | "reboundsTotal": 4, 1486 | "steals": 1, 1487 | "threePointersAttempted": 1, 1488 | "threePointersMade": 1, 1489 | "threePointersPercentage": 1.0, 1490 | "turnovers": 0, 1491 | "twoPointersAttempted": 1, 1492 | "twoPointersMade": 0, 1493 | "twoPointersPercentage": 0.0 1494 | }, 1495 | "name": "R.J. Hampton", 1496 | "nameI": "R. Hampton", 1497 | "firstName": "R.J.", 1498 | "familyName": "Hampton" 1499 | }, 1500 | { 1501 | "status": "ACTIVE", 1502 | "notPlayingReason": "DNP_INJURY", 1503 | "notPlayingDescription": "N/A; Illness", 1504 | "order": 11, 1505 | "personId": 1630175, 1506 | "jerseyNum": "50", 1507 | "starter": "0", 1508 | "oncourt": "0", 1509 | "played": "0", 1510 | "statistics": { 1511 | "assists": 0, 1512 | "blocks": 0, 1513 | "blocksReceived": 0, 1514 | "fieldGoalsAttempted": 0, 1515 | "fieldGoalsMade": 0, 1516 | "fieldGoalsPercentage": 0.0, 1517 | "foulsOffensive": 0, 1518 | "foulsDrawn": 0, 1519 | "foulsPersonal": 0, 1520 | "foulsTechnical": 0, 1521 | "freeThrowsAttempted": 0, 1522 | "freeThrowsMade": 0, 1523 | "freeThrowsPercentage": 0.0, 1524 | "minus": 0.0, 1525 | "minutes": "PT00M00.00S", 1526 | "minutesCalculated": "PT00M", 1527 | "plus": 0.0, 1528 | "plusMinusPoints": 0.0, 1529 | "points": 0, 1530 | "pointsFastBreak": 0, 1531 | "pointsInThePaint": 0, 1532 | "pointsSecondChance": 0, 1533 | "reboundsDefensive": 0, 1534 | "reboundsOffensive": 0, 1535 | "reboundsTotal": 0, 1536 | "steals": 0, 1537 | "threePointersAttempted": 0, 1538 | "threePointersMade": 0, 1539 | "threePointersPercentage": 0.0, 1540 | "turnovers": 0, 1541 | "twoPointersAttempted": 0, 1542 | "twoPointersMade": 0, 1543 | "twoPointersPercentage": 0.0 1544 | }, 1545 | "name": "Cole Anthony", 1546 | "nameI": "C. Anthony", 1547 | "firstName": "Cole", 1548 | "familyName": "Anthony" 1549 | }, 1550 | { 1551 | "status": "ACTIVE", 1552 | "order": 12, 1553 | "personId": 1630284, 1554 | "jerseyNum": "7", 1555 | "starter": "0", 1556 | "oncourt": "0", 1557 | "played": "0", 1558 | "statistics": { 1559 | "assists": 0, 1560 | "blocks": 0, 1561 | "blocksReceived": 0, 1562 | "fieldGoalsAttempted": 0, 1563 | "fieldGoalsMade": 0, 1564 | "fieldGoalsPercentage": 0.0, 1565 | "foulsOffensive": 0, 1566 | "foulsDrawn": 0, 1567 | "foulsPersonal": 0, 1568 | "foulsTechnical": 0, 1569 | "freeThrowsAttempted": 0, 1570 | "freeThrowsMade": 0, 1571 | "freeThrowsPercentage": 0.0, 1572 | "minus": 0.0, 1573 | "minutes": "PT00M00.00S", 1574 | "minutesCalculated": "PT00M", 1575 | "plus": 0.0, 1576 | "plusMinusPoints": 0.0, 1577 | "points": 0, 1578 | "pointsFastBreak": 0, 1579 | "pointsInThePaint": 0, 1580 | "pointsSecondChance": 0, 1581 | "reboundsDefensive": 0, 1582 | "reboundsOffensive": 0, 1583 | "reboundsTotal": 0, 1584 | "steals": 0, 1585 | "threePointersAttempted": 0, 1586 | "threePointersMade": 0, 1587 | "threePointersPercentage": 0.0, 1588 | "turnovers": 0, 1589 | "twoPointersAttempted": 0, 1590 | "twoPointersMade": 0, 1591 | "twoPointersPercentage": 0.0 1592 | }, 1593 | "name": "Kevon Harris", 1594 | "nameI": "K. Harris", 1595 | "firstName": "Kevon", 1596 | "familyName": "Harris" 1597 | }, 1598 | { 1599 | "status": "ACTIVE", 1600 | "order": 13, 1601 | "personId": 1629678, 1602 | "jerseyNum": "25", 1603 | "starter": "0", 1604 | "oncourt": "0", 1605 | "played": "0", 1606 | "statistics": { 1607 | "assists": 0, 1608 | "blocks": 0, 1609 | "blocksReceived": 0, 1610 | "fieldGoalsAttempted": 0, 1611 | "fieldGoalsMade": 0, 1612 | "fieldGoalsPercentage": 0.0, 1613 | "foulsOffensive": 0, 1614 | "foulsDrawn": 0, 1615 | "foulsPersonal": 0, 1616 | "foulsTechnical": 0, 1617 | "freeThrowsAttempted": 0, 1618 | "freeThrowsMade": 0, 1619 | "freeThrowsPercentage": 0.0, 1620 | "minus": 0.0, 1621 | "minutes": "PT00M00.00S", 1622 | "minutesCalculated": "PT00M", 1623 | "plus": 0.0, 1624 | "plusMinusPoints": 0.0, 1625 | "points": 0, 1626 | "pointsFastBreak": 0, 1627 | "pointsInThePaint": 0, 1628 | "pointsSecondChance": 0, 1629 | "reboundsDefensive": 0, 1630 | "reboundsOffensive": 0, 1631 | "reboundsTotal": 0, 1632 | "steals": 0, 1633 | "threePointersAttempted": 0, 1634 | "threePointersMade": 0, 1635 | "threePointersPercentage": 0.0, 1636 | "turnovers": 0, 1637 | "twoPointersAttempted": 0, 1638 | "twoPointersMade": 0, 1639 | "twoPointersPercentage": 0.0 1640 | }, 1641 | "name": "Admiral Schofield", 1642 | "nameI": "A. Schofield", 1643 | "firstName": "Admiral", 1644 | "familyName": "Schofield" 1645 | }, 1646 | { 1647 | "status": "INACTIVE", 1648 | "notPlayingReason": "INACTIVE_INJURY", 1649 | "notPlayingDescription": "Left Big toe; Fracture", 1650 | "order": 14, 1651 | "personId": 1628365, 1652 | "jerseyNum": "20", 1653 | "starter": "0", 1654 | "oncourt": "0", 1655 | "played": "0", 1656 | "statistics": { 1657 | "assists": 0, 1658 | "blocks": 0, 1659 | "blocksReceived": 0, 1660 | "fieldGoalsAttempted": 0, 1661 | "fieldGoalsMade": 0, 1662 | "fieldGoalsPercentage": 0.0, 1663 | "foulsOffensive": 0, 1664 | "foulsDrawn": 0, 1665 | "foulsPersonal": 0, 1666 | "foulsTechnical": 0, 1667 | "freeThrowsAttempted": 0, 1668 | "freeThrowsMade": 0, 1669 | "freeThrowsPercentage": 0.0, 1670 | "minus": 0.0, 1671 | "minutes": "PT00M00.00S", 1672 | "minutesCalculated": "PT00M", 1673 | "plus": 0.0, 1674 | "plusMinusPoints": 0.0, 1675 | "points": 0, 1676 | "pointsFastBreak": 0, 1677 | "pointsInThePaint": 0, 1678 | "pointsSecondChance": 0, 1679 | "reboundsDefensive": 0, 1680 | "reboundsOffensive": 0, 1681 | "reboundsTotal": 0, 1682 | "steals": 0, 1683 | "threePointersAttempted": 0, 1684 | "threePointersMade": 0, 1685 | "threePointersPercentage": 0.0, 1686 | "turnovers": 0, 1687 | "twoPointersAttempted": 0, 1688 | "twoPointersMade": 0, 1689 | "twoPointersPercentage": 0.0 1690 | }, 1691 | "name": "Markelle Fultz", 1692 | "nameI": "M. Fultz", 1693 | "firstName": "Markelle", 1694 | "familyName": "Fultz" 1695 | }, 1696 | { 1697 | "status": "INACTIVE", 1698 | "notPlayingReason": "INACTIVE_INJURY", 1699 | "notPlayingDescription": "Left Knee; Injury recovery", 1700 | "order": 15, 1701 | "personId": 203914, 1702 | "jerseyNum": "14", 1703 | "starter": "0", 1704 | "oncourt": "0", 1705 | "played": "0", 1706 | "statistics": { 1707 | "assists": 0, 1708 | "blocks": 0, 1709 | "blocksReceived": 0, 1710 | "fieldGoalsAttempted": 0, 1711 | "fieldGoalsMade": 0, 1712 | "fieldGoalsPercentage": 0.0, 1713 | "foulsOffensive": 0, 1714 | "foulsDrawn": 0, 1715 | "foulsPersonal": 0, 1716 | "foulsTechnical": 0, 1717 | "freeThrowsAttempted": 0, 1718 | "freeThrowsMade": 0, 1719 | "freeThrowsPercentage": 0.0, 1720 | "minus": 0.0, 1721 | "minutes": "PT00M00.00S", 1722 | "minutesCalculated": "PT00M", 1723 | "plus": 0.0, 1724 | "plusMinusPoints": 0.0, 1725 | "points": 0, 1726 | "pointsFastBreak": 0, 1727 | "pointsInThePaint": 0, 1728 | "pointsSecondChance": 0, 1729 | "reboundsDefensive": 0, 1730 | "reboundsOffensive": 0, 1731 | "reboundsTotal": 0, 1732 | "steals": 0, 1733 | "threePointersAttempted": 0, 1734 | "threePointersMade": 0, 1735 | "threePointersPercentage": 0.0, 1736 | "turnovers": 0, 1737 | "twoPointersAttempted": 0, 1738 | "twoPointersMade": 0, 1739 | "twoPointersPercentage": 0.0 1740 | }, 1741 | "name": "Gary Harris", 1742 | "nameI": "G. Harris", 1743 | "firstName": "Gary", 1744 | "familyName": "Harris" 1745 | }, 1746 | { 1747 | "status": "INACTIVE", 1748 | "notPlayingReason": "INACTIVE_INJURY", 1749 | "notPlayingDescription": "Left Knee; Injury recovery", 1750 | "order": 16, 1751 | "personId": 1628371, 1752 | "jerseyNum": "1", 1753 | "starter": "0", 1754 | "oncourt": "0", 1755 | "played": "0", 1756 | "statistics": { 1757 | "assists": 0, 1758 | "blocks": 0, 1759 | "blocksReceived": 0, 1760 | "fieldGoalsAttempted": 0, 1761 | "fieldGoalsMade": 0, 1762 | "fieldGoalsPercentage": 0.0, 1763 | "foulsOffensive": 0, 1764 | "foulsDrawn": 0, 1765 | "foulsPersonal": 0, 1766 | "foulsTechnical": 0, 1767 | "freeThrowsAttempted": 0, 1768 | "freeThrowsMade": 0, 1769 | "freeThrowsPercentage": 0.0, 1770 | "minus": 0.0, 1771 | "minutes": "PT00M00.00S", 1772 | "minutesCalculated": "PT00M", 1773 | "plus": 0.0, 1774 | "plusMinusPoints": 0.0, 1775 | "points": 0, 1776 | "pointsFastBreak": 0, 1777 | "pointsInThePaint": 0, 1778 | "pointsSecondChance": 0, 1779 | "reboundsDefensive": 0, 1780 | "reboundsOffensive": 0, 1781 | "reboundsTotal": 0, 1782 | "steals": 0, 1783 | "threePointersAttempted": 0, 1784 | "threePointersMade": 0, 1785 | "threePointersPercentage": 0.0, 1786 | "turnovers": 0, 1787 | "twoPointersAttempted": 0, 1788 | "twoPointersMade": 0, 1789 | "twoPointersPercentage": 0.0 1790 | }, 1791 | "name": "Jonathan Isaac", 1792 | "nameI": "J. Isaac", 1793 | "firstName": "Jonathan", 1794 | "familyName": "Isaac" 1795 | }, 1796 | { 1797 | "status": "INACTIVE", 1798 | "notPlayingReason": "INACTIVE_INJURY", 1799 | "notPlayingDescription": "Right Midfoot; Sprain", 1800 | "order": 17, 1801 | "personId": 1629021, 1802 | "jerseyNum": "21", 1803 | "starter": "0", 1804 | "oncourt": "0", 1805 | "played": "0", 1806 | "statistics": { 1807 | "assists": 0, 1808 | "blocks": 0, 1809 | "blocksReceived": 0, 1810 | "fieldGoalsAttempted": 0, 1811 | "fieldGoalsMade": 0, 1812 | "fieldGoalsPercentage": 0.0, 1813 | "foulsOffensive": 0, 1814 | "foulsDrawn": 0, 1815 | "foulsPersonal": 0, 1816 | "foulsTechnical": 0, 1817 | "freeThrowsAttempted": 0, 1818 | "freeThrowsMade": 0, 1819 | "freeThrowsPercentage": 0.0, 1820 | "minus": 0.0, 1821 | "minutes": "PT00M00.00S", 1822 | "minutesCalculated": "PT00M", 1823 | "plus": 0.0, 1824 | "plusMinusPoints": 0.0, 1825 | "points": 0, 1826 | "pointsFastBreak": 0, 1827 | "pointsInThePaint": 0, 1828 | "pointsSecondChance": 0, 1829 | "reboundsDefensive": 0, 1830 | "reboundsOffensive": 0, 1831 | "reboundsTotal": 0, 1832 | "steals": 0, 1833 | "threePointersAttempted": 0, 1834 | "threePointersMade": 0, 1835 | "threePointersPercentage": 0.0, 1836 | "turnovers": 0, 1837 | "twoPointersAttempted": 0, 1838 | "twoPointersMade": 0, 1839 | "twoPointersPercentage": 0.0 1840 | }, 1841 | "name": "Moritz Wagner", 1842 | "nameI": "M. Wagner", 1843 | "firstName": "Moritz", 1844 | "familyName": "Wagner" 1845 | } 1846 | ], 1847 | "statistics": { 1848 | "assists": 21, 1849 | "assistsTurnoverRatio": 1.16666666666667, 1850 | "benchPoints": 17, 1851 | "biggestLead": 15, 1852 | "biggestLeadScore": "21-6", 1853 | "biggestScoringRun": 19, 1854 | "biggestScoringRunScore": "21-6", 1855 | "blocks": 5, 1856 | "blocksReceived": 4, 1857 | "fastBreakPointsAttempted": 11, 1858 | "fastBreakPointsMade": 10, 1859 | "fastBreakPointsPercentage": 0.9090909090909091, 1860 | "fieldGoalsAttempted": 86, 1861 | "fieldGoalsEffectiveAdjusted": 0.552325581395349, 1862 | "fieldGoalsMade": 42, 1863 | "fieldGoalsPercentage": 0.48837209302325596, 1864 | "foulsOffensive": 1, 1865 | "foulsDrawn": 21, 1866 | "foulsPersonal": 24, 1867 | "foulsTeam": 23, 1868 | "foulsTechnical": 0, 1869 | "foulsTeamTechnical": 0, 1870 | "freeThrowsAttempted": 19, 1871 | "freeThrowsMade": 14, 1872 | "freeThrowsPercentage": 0.736842105263158, 1873 | "leadChanges": 7, 1874 | "minutes": "PT240M00.00S", 1875 | "minutesCalculated": "PT240M", 1876 | "points": 109, 1877 | "pointsAgainst": 113, 1878 | "pointsFastBreak": 23, 1879 | "pointsFromTurnovers": 15, 1880 | "pointsInThePaint": 56, 1881 | "pointsInThePaintAttempted": 50, 1882 | "pointsInThePaintMade": 28, 1883 | "pointsInThePaintPercentage": 0.56, 1884 | "pointsSecondChance": 16, 1885 | "reboundsDefensive": 38, 1886 | "reboundsOffensive": 10, 1887 | "reboundsPersonal": 48, 1888 | "reboundsTeam": 8, 1889 | "reboundsTeamDefensive": 3, 1890 | "reboundsTeamOffensive": 5, 1891 | "reboundsTotal": 56, 1892 | "secondChancePointsAttempted": 9, 1893 | "secondChancePointsMade": 6, 1894 | "secondChancePointsPercentage": 0.666666666666666, 1895 | "steals": 5, 1896 | "threePointersAttempted": 30, 1897 | "threePointersMade": 11, 1898 | "threePointersPercentage": 0.366666666666667, 1899 | "timeLeading": "PT19M15.00S", 1900 | "timesTied": 4, 1901 | "trueShootingAttempts": 94.36, 1902 | "trueShootingPercentage": 0.57757524374735, 1903 | "turnovers": 18, 1904 | "turnoversTeam": 0, 1905 | "turnoversTotal": 18, 1906 | "twoPointersAttempted": 56, 1907 | "twoPointersMade": 31, 1908 | "twoPointersPercentage": 0.5535714285714289 1909 | } 1910 | } 1911 | } 1912 | } 1913 | --------------------------------------------------------------------------------