add skin part caching

This commit is contained in:
Lee 2024-04-10 12:41:35 +01:00
parent 3790d4a312
commit 28cd7f192d
4 changed files with 78 additions and 8 deletions

@ -1,7 +1,6 @@
package cc.fascinated.controller; package cc.fascinated.controller;
import cc.fascinated.common.PlayerUtils; import cc.fascinated.model.cache.CachedPlayer;
import cc.fascinated.model.player.Player;
import cc.fascinated.model.player.Skin; import cc.fascinated.model.player.Skin;
import cc.fascinated.service.PlayerService; import cc.fascinated.service.PlayerService;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@ -21,11 +20,11 @@ import java.util.concurrent.TimeUnit;
public class PlayerController { public class PlayerController {
private final CacheControl cacheControl = CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic(); private final CacheControl cacheControl = CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic();
private final PlayerService playerManagerService; private final PlayerService playerService;
@Autowired @Autowired
public PlayerController(PlayerService playerManagerService) { public PlayerController(PlayerService playerManagerService) {
this.playerManagerService = playerManagerService; this.playerService = playerManagerService;
} }
@ResponseBody @ResponseBody
@ -34,7 +33,7 @@ public class PlayerController {
@Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id) { @Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id) {
return ResponseEntity.ok() return ResponseEntity.ok()
.cacheControl(cacheControl) .cacheControl(cacheControl)
.body(playerManagerService.getPlayer(id)); .body(playerService.getPlayer(id));
} }
@GetMapping(value = "/{part}/{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 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 = "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) { @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); Skin.Parts skinPart = Skin.Parts.fromName(part);
String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png"; String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png";
@ -52,6 +51,6 @@ public class PlayerController {
.cacheControl(cacheControl) .cacheControl(cacheControl)
.contentType(MediaType.IMAGE_PNG) .contentType(MediaType.IMAGE_PNG)
.header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(player.getUsername())) .header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(player.getUsername()))
.body(PlayerUtils.getSkinPartBytes(player.getSkin(), skinPart, size)); .body(playerService.getSkinPart(player, skinPart, size).getBytes());
} }
} }

@ -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;
}

@ -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.
* <p>
* This will allow us to easily lookup a
* player skin part by it's id.
* </p>
*/
public interface PlayerSkinPartCacheRepository extends CrudRepository<CachedPlayerSkinPart, String> { }

@ -8,12 +8,15 @@ import cc.fascinated.exception.impl.RateLimitException;
import cc.fascinated.exception.impl.ResourceNotFoundException; import cc.fascinated.exception.impl.ResourceNotFoundException;
import cc.fascinated.model.cache.CachedPlayer; import cc.fascinated.model.cache.CachedPlayer;
import cc.fascinated.model.cache.CachedPlayerName; import cc.fascinated.model.cache.CachedPlayerName;
import cc.fascinated.model.cache.CachedPlayerSkinPart;
import cc.fascinated.model.mojang.MojangProfile; import cc.fascinated.model.mojang.MojangProfile;
import cc.fascinated.model.mojang.MojangUsernameToUuid; import cc.fascinated.model.mojang.MojangUsernameToUuid;
import cc.fascinated.model.player.Cape; import cc.fascinated.model.player.Cape;
import cc.fascinated.model.player.Player;
import cc.fascinated.model.player.Skin; import cc.fascinated.model.player.Skin;
import cc.fascinated.repository.PlayerCacheRepository; import cc.fascinated.repository.PlayerCacheRepository;
import cc.fascinated.repository.PlayerNameCacheRepository; import cc.fascinated.repository.PlayerNameCacheRepository;
import cc.fascinated.repository.PlayerSkinPartCacheRepository;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -27,12 +30,15 @@ public class PlayerService {
private final MojangService mojangAPIService; private final MojangService mojangAPIService;
private final PlayerCacheRepository playerCacheRepository; private final PlayerCacheRepository playerCacheRepository;
private final PlayerNameCacheRepository playerNameCacheRepository; private final PlayerNameCacheRepository playerNameCacheRepository;
private final PlayerSkinPartCacheRepository playerSkinPartCacheRepository;
@Autowired @Autowired
public PlayerService(MojangService mojangAPIService, PlayerCacheRepository playerCacheRepository, PlayerNameCacheRepository playerNameCacheRepository) { public PlayerService(MojangService mojangAPIService, PlayerCacheRepository playerCacheRepository,
PlayerNameCacheRepository playerNameCacheRepository, PlayerSkinPartCacheRepository playerSkinPartCacheRepository) {
this.mojangAPIService = mojangAPIService; this.mojangAPIService = mojangAPIService;
this.playerCacheRepository = playerCacheRepository; this.playerCacheRepository = playerCacheRepository;
this.playerNameCacheRepository = playerNameCacheRepository; this.playerNameCacheRepository = playerNameCacheRepository;
this.playerSkinPartCacheRepository = playerSkinPartCacheRepository;
} }
/** /**
@ -104,4 +110,33 @@ public class PlayerService {
throw new MojangAPIRateLimitException(); 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<CachedPlayerSkinPart> 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;
}
} }