api: impl pagination for top scores endpoint and add user + leaderboard caching
Some checks failed
Deploy API / docker (17, 3.8.5) (push) Failing after 47s
Some checks failed
Deploy API / docker (17, 3.8.5) (push) Failing after 47s
This commit is contained in:
parent
64b6ef1a7f
commit
21b6de0f15
@ -80,6 +80,11 @@
|
||||
<groupId>com.konghq</groupId>
|
||||
<artifactId>unirest-modules-jackson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.jodah</groupId>
|
||||
<artifactId>expiringmap</artifactId>
|
||||
<version>0.5.11</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Libraries -->
|
||||
<dependency>
|
||||
|
108
API/src/main/java/cc/fascinated/common/PaginationBuilder.java
Normal file
108
API/src/main/java/cc/fascinated/common/PaginationBuilder.java
Normal file
@ -0,0 +1,108 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import cc.fascinated.exception.impl.BadRequestException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Getter
|
||||
public class PaginationBuilder<T> {
|
||||
/**
|
||||
* The number of items per page.
|
||||
*/
|
||||
private int itemsPerPage;
|
||||
|
||||
/**
|
||||
* The items to paginate.
|
||||
*/
|
||||
private List<T> items;
|
||||
|
||||
/**
|
||||
* Sets the number of items per page.
|
||||
*
|
||||
* @param itemsPerPage The number of items per page.
|
||||
* @return The pagination builder.
|
||||
*/
|
||||
public PaginationBuilder<T> itemsPerPage(int itemsPerPage) {
|
||||
this.itemsPerPage = itemsPerPage;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the items to paginate.
|
||||
*
|
||||
* @param getItems The items to paginate.
|
||||
* @return The pagination builder.
|
||||
*/
|
||||
public PaginationBuilder<T> fillItems(Supplier<List<T>> getItems) {
|
||||
this.items = getItems.get();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the pagination.
|
||||
*
|
||||
* @return The pagination.
|
||||
*/
|
||||
public PaginationBuilder<T> build() {
|
||||
return new PaginationBuilder<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a page of items.
|
||||
*
|
||||
* @param page The page number.
|
||||
* @return The page.
|
||||
*/
|
||||
public Page<T> getPage(int page) {
|
||||
int totalItems = this.items.size();
|
||||
int totalPages = (int) Math.ceil((double) totalItems / this.itemsPerPage);
|
||||
|
||||
if (page < 1 || page > totalPages) {
|
||||
throw new BadRequestException("Invalid page number");
|
||||
}
|
||||
List<T> items = this.items.subList((page - 1) * this.itemsPerPage, Math.min(page * this.itemsPerPage, totalItems));
|
||||
return new Page<>(
|
||||
items,
|
||||
new Page.Metadata(page, totalPages, totalItems)
|
||||
);
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public static class Page<T> {
|
||||
/**
|
||||
* The items on the page.
|
||||
*/
|
||||
private final List<T> items;
|
||||
|
||||
/**
|
||||
* The metadata of the page.
|
||||
*/
|
||||
private final Metadata metadata;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public static class Metadata {
|
||||
/**
|
||||
* The page number.
|
||||
*/
|
||||
private final int page;
|
||||
|
||||
/**
|
||||
* The total number of pages.
|
||||
*/
|
||||
private final int totalPages;
|
||||
|
||||
/**
|
||||
* The total number of items.
|
||||
*/
|
||||
private final int totalItems;
|
||||
}
|
||||
}
|
||||
}
|
@ -9,8 +9,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@ -45,8 +43,12 @@ public class ScoresController {
|
||||
*/
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/top/{platform}")
|
||||
public ResponseEntity<List<?>> getTopScores(@PathVariable String platform, @RequestParam(defaultValue = "false") boolean scoresonly) {
|
||||
return ResponseEntity.ok(scoreService.getTopRankedScores(Platform.Platforms.getPlatform(platform), 50, scoresonly));
|
||||
public ResponseEntity<?> getTopScores(
|
||||
@PathVariable String platform,
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "false") boolean scoresonly
|
||||
) {
|
||||
return ResponseEntity.ok(scoreService.getTopRankedScores(Platform.Platforms.getPlatform(platform), page, scoresonly));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -78,6 +78,7 @@ public class Score {
|
||||
* e.g. 500pp
|
||||
* </p>
|
||||
*/
|
||||
@Indexed
|
||||
private Double pp;
|
||||
|
||||
/**
|
||||
|
@ -24,7 +24,7 @@ public interface ScoreRepository extends MongoRepository<Score, Long> {
|
||||
@Aggregation(pipeline = {
|
||||
"{ $match: { platform: ?0, pp: { $gt: 0 } } }",
|
||||
"{ $sort: { pp: -1 } }",
|
||||
"{ $limit: ?1 }"
|
||||
"{ $limit: ?1 }",
|
||||
})
|
||||
List<Score> getTopRankedScores(@NonNull Platform.Platforms platform, int amount);
|
||||
|
||||
|
@ -10,12 +10,16 @@ import cc.fascinated.repository.ScoreSaberLeaderboardRepository;
|
||||
import kong.unirest.core.HttpResponse;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import net.jodah.expiringmap.ExpirationPolicy;
|
||||
import net.jodah.expiringmap.ExpiringMap;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
@ -28,6 +32,12 @@ public class ScoreSaberService {
|
||||
private static final String GET_LEADERBOARD_ENDPOINT = SCORESABER_API + "leaderboard/by-id/%s/info";
|
||||
private static final String GET_RANKED_LEADERBOARDS_ENDPOINT = SCORESABER_API + "leaderboards?ranked=true&page=%s&withMetadata=true";
|
||||
|
||||
private final Map<String, ScoreSaberLeaderboardToken> leaderboardCache = ExpiringMap.builder()
|
||||
.maxSize(5_000)
|
||||
.expirationPolicy(ExpirationPolicy.CREATED)
|
||||
.expiration(30, TimeUnit.MINUTES)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* The ScoreSaber leaderboard repository to use.
|
||||
*/
|
||||
@ -69,9 +79,15 @@ public class ScoreSaberService {
|
||||
* @throws BadRequestException if an error occurred while getting the leaderboard
|
||||
*/
|
||||
public ScoreSaberLeaderboardToken getLeaderboard(String leaderboardId, boolean bypassCache) {
|
||||
if (leaderboardCache.containsKey(leaderboardId) && !bypassCache) { // The leaderboard is cached
|
||||
return leaderboardCache.get(leaderboardId);
|
||||
}
|
||||
|
||||
Optional<ScoreSaberLeaderboardToken> leaderboardOptional = leaderboardRepository.findById(leaderboardId);
|
||||
if (leaderboardOptional.isPresent() && !bypassCache) { // The leaderboard is cached
|
||||
return leaderboardOptional.get();
|
||||
ScoreSaberLeaderboardToken leaderboard = leaderboardOptional.get();
|
||||
leaderboardCache.put(leaderboardId, leaderboard);
|
||||
return leaderboard;
|
||||
}
|
||||
|
||||
HttpResponse<ScoreSaberLeaderboardToken> response = Request.get(GET_LEADERBOARD_ENDPOINT.formatted(leaderboardId), ScoreSaberLeaderboardToken.class);
|
||||
@ -83,6 +99,7 @@ public class ScoreSaberService {
|
||||
}
|
||||
ScoreSaberLeaderboardToken leaderboard = response.getBody();
|
||||
leaderboardRepository.save(leaderboard);
|
||||
leaderboardCache.put(leaderboardId, leaderboard);
|
||||
return leaderboard;
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ package cc.fascinated.services;
|
||||
import cc.fascinated.common.DateUtils;
|
||||
import cc.fascinated.common.EnumUtils;
|
||||
import cc.fascinated.common.MathUtils;
|
||||
import cc.fascinated.common.PaginationBuilder;
|
||||
import cc.fascinated.model.leaderboard.Leaderboard;
|
||||
import cc.fascinated.model.score.DeviceInformation;
|
||||
import cc.fascinated.model.score.Score;
|
||||
@ -74,14 +75,16 @@ public class ScoreService {
|
||||
* Gets the top ranked scores from the platform.
|
||||
*
|
||||
* @param platform The platform to get the scores from.
|
||||
* @param amount The amount of scores to get.
|
||||
* @param scoresOnly Whether to only get the scores.
|
||||
* @return The scores.
|
||||
*/
|
||||
public List<ScoreSaberScoreResponse> getTopRankedScores(@NonNull Platform.Platforms platform, int amount, boolean scoresOnly) {
|
||||
List<Score> trackedScores = scoreRepository.getTopRankedScores(platform, amount);
|
||||
public PaginationBuilder.Page<ScoreSaberScoreResponse> getTopRankedScores(@NonNull Platform.Platforms platform, int pageNumber, boolean scoresOnly) {
|
||||
PaginationBuilder<ScoreSaberScoreResponse> builder = new PaginationBuilder<ScoreSaberScoreResponse>().build();
|
||||
builder.itemsPerPage(50);
|
||||
builder.fillItems(() -> {
|
||||
List<Score> foundScores = this.scoreRepository.getTopRankedScores(platform, 250);
|
||||
List<ScoreSaberScoreResponse> scores = new ArrayList<>();
|
||||
for (Score score : trackedScores) {
|
||||
for (Score score : foundScores) {
|
||||
ScoreSaberScore scoreSaberScore = (ScoreSaberScore) score;
|
||||
UserDTO user = scoresOnly ? null : userService.getUser(score.getPlayerId()).getAsDTO();
|
||||
Leaderboard leaderboard = scoresOnly ? null : Leaderboard.getFromScoreSaberToken(scoreSaberService.getLeaderboard(score.getLeaderboardId()));
|
||||
@ -108,8 +111,9 @@ public class ScoreService {
|
||||
leaderboard
|
||||
));
|
||||
}
|
||||
|
||||
return scores;
|
||||
});
|
||||
return builder.getPage(pageNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -245,7 +249,8 @@ public class ScoreService {
|
||||
* @param leaderboardId The leaderboard id to get the score from.
|
||||
* @return The previous score.
|
||||
*/
|
||||
public @NonNull List<Score> getScoreHistory(@NonNull Platform.Platforms platform, @NonNull User user, @NonNull String leaderboardId) {
|
||||
public @NonNull List<Score> getScoreHistory(@NonNull Platform.Platforms platform, @NonNull User
|
||||
user, @NonNull String leaderboardId) {
|
||||
List<Score> foundScores = new ArrayList<>(this.scoreRepository.findScores(platform, user.getSteamId(), leaderboardId));
|
||||
|
||||
// Sort previous scores by timestamp (newest -> oldest)
|
||||
@ -280,7 +285,8 @@ public class ScoreService {
|
||||
* @param score The score to log.
|
||||
* @param user The user who set the score.
|
||||
*/
|
||||
private void logScore(@NonNull Platform.Platforms platform, @NonNull Leaderboard leaderboard, @NonNull Score score,
|
||||
private void logScore(@NonNull Platform.Platforms platform, @NonNull Leaderboard leaderboard, @NonNull Score
|
||||
score,
|
||||
@NonNull User user) {
|
||||
String platformName = EnumUtils.getEnumName(platform);
|
||||
boolean isRanked = score.getPp() != 0;
|
||||
|
@ -9,13 +9,12 @@ import cc.fascinated.model.user.history.HistoryPoint;
|
||||
import cc.fascinated.repository.UserRepository;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import net.jodah.expiringmap.ExpirationPolicy;
|
||||
import net.jodah.expiringmap.ExpiringMap;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@ -41,6 +40,15 @@ public class UserService {
|
||||
@NonNull
|
||||
private final ScoreSaberService scoreSaberService;
|
||||
|
||||
/**
|
||||
* The user cache to use
|
||||
*/
|
||||
private final Map<String, User> userCache = ExpiringMap.builder()
|
||||
.maxSize(5_000)
|
||||
.expirationPolicy(ExpirationPolicy.ACCESSED)
|
||||
.expiration(1, TimeUnit.HOURS)
|
||||
.build();
|
||||
|
||||
@Autowired
|
||||
public UserService(@NonNull UserRepository userRepository, @NonNull ScoreSaberService scoreSaberService) {
|
||||
this.userRepository = userRepository;
|
||||
@ -58,6 +66,10 @@ public class UserService {
|
||||
if (!this.isValidSteamId(steamId)) {
|
||||
throw new BadRequestException("Invalid steam id");
|
||||
}
|
||||
if (this.userCache.containsKey(steamId)) {
|
||||
return this.userCache.get(steamId);
|
||||
}
|
||||
|
||||
Optional<User> userOptional = this.userRepository.findBySteamId(steamId);
|
||||
User user;
|
||||
boolean shouldUpdate = false;
|
||||
@ -94,6 +106,7 @@ public class UserService {
|
||||
if (shouldUpdate) {
|
||||
this.saveUser(user); // Save the user
|
||||
}
|
||||
this.userCache.put(steamId, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user