From 28cd7f192d0a54f38156b2a460724190c42edcb0 Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 10 Apr 2024 12:41:35 +0100 Subject: [PATCH] add skin part caching --- .../controller/PlayerController.java | 13 +++---- .../model/cache/CachedPlayerSkinPart.java | 22 +++++++++++ .../PlayerSkinPartCacheRepository.java | 14 +++++++ .../cc.fascinated/service/PlayerService.java | 37 ++++++++++++++++++- 4 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 src/main/java/cc.fascinated/model/cache/CachedPlayerSkinPart.java create mode 100644 src/main/java/cc.fascinated/repository/PlayerSkinPartCacheRepository.java diff --git a/src/main/java/cc.fascinated/controller/PlayerController.java b/src/main/java/cc.fascinated/controller/PlayerController.java index 52d9c24..6e49693 100644 --- a/src/main/java/cc.fascinated/controller/PlayerController.java +++ b/src/main/java/cc.fascinated/controller/PlayerController.java @@ -1,7 +1,6 @@ package cc.fascinated.controller; -import cc.fascinated.common.PlayerUtils; -import cc.fascinated.model.player.Player; +import cc.fascinated.model.cache.CachedPlayer; import cc.fascinated.model.player.Skin; import cc.fascinated.service.PlayerService; import io.swagger.v3.oas.annotations.Parameter; @@ -21,11 +20,11 @@ import java.util.concurrent.TimeUnit; public class PlayerController { private final CacheControl cacheControl = CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic(); - private final PlayerService playerManagerService; + private final PlayerService playerService; @Autowired public PlayerController(PlayerService playerManagerService) { - this.playerManagerService = playerManagerService; + this.playerService = playerManagerService; } @ResponseBody @@ -34,7 +33,7 @@ public class PlayerController { @Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id) { return ResponseEntity.ok() .cacheControl(cacheControl) - .body(playerManagerService.getPlayer(id)); + .body(playerService.getPlayer(id)); } @GetMapping(value = "/{part}/{id}") @@ -43,7 +42,7 @@ public class PlayerController { @Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id, @Parameter(description = "The size of the image", example = "256") @RequestParam(required = false, defaultValue = "256") int size, @Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download) { - Player player = playerManagerService.getPlayer(id); + CachedPlayer player = playerService.getPlayer(id); Skin.Parts skinPart = Skin.Parts.fromName(part); String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png"; @@ -52,6 +51,6 @@ public class PlayerController { .cacheControl(cacheControl) .contentType(MediaType.IMAGE_PNG) .header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(player.getUsername())) - .body(PlayerUtils.getSkinPartBytes(player.getSkin(), skinPart, size)); + .body(playerService.getSkinPart(player, skinPart, size).getBytes()); } } diff --git a/src/main/java/cc.fascinated/model/cache/CachedPlayerSkinPart.java b/src/main/java/cc.fascinated/model/cache/CachedPlayerSkinPart.java new file mode 100644 index 0000000..a97bf2c --- /dev/null +++ b/src/main/java/cc.fascinated/model/cache/CachedPlayerSkinPart.java @@ -0,0 +1,22 @@ +package cc.fascinated.model.cache; + +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +@Setter +@Getter +@AllArgsConstructor +@RedisHash(value = "player", timeToLive = 60L * 60L) // 1 hour (in seconds) +public class CachedPlayerSkinPart { + + /** + * The ID of the skin part + */ + @Id @NonNull private String id; + + /** + * The skin part bytes + */ + private byte[] bytes; +} diff --git a/src/main/java/cc.fascinated/repository/PlayerSkinPartCacheRepository.java b/src/main/java/cc.fascinated/repository/PlayerSkinPartCacheRepository.java new file mode 100644 index 0000000..b23d18e --- /dev/null +++ b/src/main/java/cc.fascinated/repository/PlayerSkinPartCacheRepository.java @@ -0,0 +1,14 @@ +package cc.fascinated.repository; + +import cc.fascinated.model.cache.CachedPlayerName; +import cc.fascinated.model.cache.CachedPlayerSkinPart; +import org.springframework.data.repository.CrudRepository; + +/** + * A cache repository for player skin parts. + *

+ * This will allow us to easily lookup a + * player skin part by it's id. + *

+ */ +public interface PlayerSkinPartCacheRepository extends CrudRepository { } \ No newline at end of file diff --git a/src/main/java/cc.fascinated/service/PlayerService.java b/src/main/java/cc.fascinated/service/PlayerService.java index 0d77163..1f7ae19 100644 --- a/src/main/java/cc.fascinated/service/PlayerService.java +++ b/src/main/java/cc.fascinated/service/PlayerService.java @@ -8,12 +8,15 @@ import cc.fascinated.exception.impl.RateLimitException; import cc.fascinated.exception.impl.ResourceNotFoundException; import cc.fascinated.model.cache.CachedPlayer; import cc.fascinated.model.cache.CachedPlayerName; +import cc.fascinated.model.cache.CachedPlayerSkinPart; import cc.fascinated.model.mojang.MojangProfile; import cc.fascinated.model.mojang.MojangUsernameToUuid; import cc.fascinated.model.player.Cape; +import cc.fascinated.model.player.Player; import cc.fascinated.model.player.Skin; import cc.fascinated.repository.PlayerCacheRepository; import cc.fascinated.repository.PlayerNameCacheRepository; +import cc.fascinated.repository.PlayerSkinPartCacheRepository; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -27,12 +30,15 @@ public class PlayerService { private final MojangService mojangAPIService; private final PlayerCacheRepository playerCacheRepository; private final PlayerNameCacheRepository playerNameCacheRepository; + private final PlayerSkinPartCacheRepository playerSkinPartCacheRepository; @Autowired - public PlayerService(MojangService mojangAPIService, PlayerCacheRepository playerCacheRepository, PlayerNameCacheRepository playerNameCacheRepository) { + public PlayerService(MojangService mojangAPIService, PlayerCacheRepository playerCacheRepository, + PlayerNameCacheRepository playerNameCacheRepository, PlayerSkinPartCacheRepository playerSkinPartCacheRepository) { this.mojangAPIService = mojangAPIService; this.playerCacheRepository = playerCacheRepository; this.playerNameCacheRepository = playerNameCacheRepository; + this.playerSkinPartCacheRepository = playerSkinPartCacheRepository; } /** @@ -104,4 +110,33 @@ public class PlayerService { throw new MojangAPIRateLimitException(); } } + + /** + * Gets a skin part from the player's skin. + * + * @param player the player + * @param part the part of the skin + * @return the skin part + */ + public CachedPlayerSkinPart getSkinPart(Player player, Skin.Parts part, int size) { + log.info("Getting skin part: {} for player: {}", part.getName(), player.getUuid()); + String key = "%s-%s-%s".formatted(player.getUuid(), part.getName(), size); + Optional cache = playerSkinPartCacheRepository.findById(key); + + // The skin part is cached + if (cache.isPresent()) { + log.info("Skin part {} for player {} is cached", part.getName(), player.getUuid()); + return cache.get(); + } + + byte[] skinPartBytes = PlayerUtils.getSkinPartBytes(player.getSkin(), part, size); + CachedPlayerSkinPart skinPart = new CachedPlayerSkinPart( + key, + skinPartBytes + ); + log.info("Fetched skin part: {} for player: {}", part.getName(), player.getUuid()); + + playerSkinPartCacheRepository.save(skinPart); + return skinPart; + } }