diff --git a/src/main/java/cc/fascinated/common/Request.java b/src/main/java/cc/fascinated/common/Request.java new file mode 100644 index 0000000..5e606fb --- /dev/null +++ b/src/main/java/cc/fascinated/common/Request.java @@ -0,0 +1,90 @@ +package cc.fascinated.common; + +import kong.unirest.core.Headers; +import kong.unirest.core.HttpResponse; +import kong.unirest.core.Unirest; +import lombok.extern.log4j.Log4j2; + +import java.util.List; + +/** + * @author Fascinated (fascinated7) + */ +@Log4j2(topic = "Request") +public class Request { + /** + * The rate limit headers. + */ + private static final List rateLimitHeaders = List.of( + "X-RateLimit-Remaining", + "RateLimit-Remaining" + ); + + /** + * The rate limit reset headers. + */ + private static final List rateLimitResetHeaders = List.of( + "X-RateLimit-Reset", + "RateLimit-Reset" + ); + + /** + * Sends a GET request to a URL. + * + * @param url the URL to send the request to + * @param clazz the class to parse the response to + * @param the type of the response + * @return the response + */ + public static HttpResponse get(String url, Class clazz) { + HttpResponse response = Unirest.get(url).asObject(clazz); + int rateLimitRemaining = getRateLimitRemaining(response); + if (rateLimitRemaining == 0) { + long rateLimitReset = getRateLimitReset(response); + long timeLeft = rateLimitReset - System.currentTimeMillis(); + try { + Thread.sleep(timeLeft); + } catch (InterruptedException e) { + log.error("Failed to sleep for rate limit reset", e); + } + response = Unirest.get(url).asObject(clazz); + } + return response; + } + + /** + * Gets the rate limit remaining. + * + * @param response the response to get the rate limit remaining from + * @return the rate limit remaining + */ + public static int getRateLimitRemaining(HttpResponse response) { + Headers headers = response.getHeaders(); + for (String rateLimitHeader : rateLimitHeaders) { + if (headers.containsKey(rateLimitHeader)) { + return Integer.parseInt(headers.getFirst(rateLimitHeader)); + } + } + return -1; + } + + /** + * Gets the rate limit reset absolute time. + * + * @param response the response to get the rate limit reset time from + * @return the rate limit reset time + */ + public static long getRateLimitReset(HttpResponse response) { + Headers headers = response.getHeaders(); + for (String rateLimitResetHeader : rateLimitResetHeaders) { + if (headers.containsKey(rateLimitResetHeader)) { + long reset = Long.parseLong(headers.getFirst(rateLimitResetHeader)); + if (reset < 86400) {// Assume it's in seconds left + return System.currentTimeMillis() + reset * 1000; + } + return reset * 1000; // Assume it's in seconds + } + } + return -1; + } +} diff --git a/src/main/java/cc/fascinated/platform/impl/ScoreSaberPlatform.java b/src/main/java/cc/fascinated/platform/impl/ScoreSaberPlatform.java index 56c18b9..d5a8fb1 100644 --- a/src/main/java/cc/fascinated/platform/impl/ScoreSaberPlatform.java +++ b/src/main/java/cc/fascinated/platform/impl/ScoreSaberPlatform.java @@ -30,7 +30,7 @@ public class ScoreSaberPlatform extends Platform { /** * Delay in ms for requests per minute. */ - private static final long UPDATE_DELAY = 1000L / 250L; // 250 requests per minute + private static final long UPDATE_DELAY = 1000L / 150L; // 150 requests per minute /** * The base multiplier for stars. diff --git a/src/main/java/cc/fascinated/services/ScoreSaberService.java b/src/main/java/cc/fascinated/services/ScoreSaberService.java index 6111584..1b45702 100644 --- a/src/main/java/cc/fascinated/services/ScoreSaberService.java +++ b/src/main/java/cc/fascinated/services/ScoreSaberService.java @@ -1,5 +1,6 @@ package cc.fascinated.services; +import cc.fascinated.common.Request; import cc.fascinated.exception.impl.BadRequestException; import cc.fascinated.model.token.ScoreSaberAccountToken; import cc.fascinated.model.token.ScoreSaberLeaderboardToken; @@ -45,8 +46,7 @@ public class ScoreSaberService { throw new BadRequestException("%s does not have a linked ScoreSaber account".formatted(user.getUsername())); } - HttpResponse response = Unirest.get(GET_PLAYER_ENDPOINT.formatted(user.getSteamId())) - .asObject(ScoreSaberAccountToken.class); + HttpResponse response = Request.get(GET_PLAYER_ENDPOINT.formatted(user.getSteamId()), ScoreSaberAccountToken.class); if (response.getParsingError().isPresent()) { // Failed to parse the response throw new BadRequestException("Failed to parse ScoreSaber account for '%s'".formatted(user.getUsername())); } @@ -69,8 +69,7 @@ public class ScoreSaberService { return leaderboardOptional.get(); } - HttpResponse response = Unirest.get(GET_LEADERBOARD_ENDPOINT.formatted(leaderboardId)) - .asObject(ScoreSaberLeaderboardToken.class); + HttpResponse response = Request.get(GET_LEADERBOARD_ENDPOINT.formatted(leaderboardId), ScoreSaberLeaderboardToken.class); if (response.getParsingError().isPresent()) { // Failed to parse the response throw new BadRequestException("Failed to parse ScoreSaber leaderboard for '%s'".formatted(leaderboardId)); }