cleanup ranked score updating
All checks were successful
Deploy to Dokku / docker (ubuntu-latest) (push) Successful in 50s

This commit is contained in:
Lee 2024-08-01 02:00:47 +01:00
parent 1d4e19bd3c
commit 4e828f2c2b
5 changed files with 138 additions and 32 deletions

@ -0,0 +1,19 @@
package cc.fascinated.model.token;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter @Setter
@ToString
public class ScoreSaberLeaderboardPageToken {
/**
* The scores on this page.
*/
private ScoreSaberLeaderboardToken[] leaderboards;
/**
* The metadata for this page.
*/
private ScoreSaberPageMetadataToken metadata;
}

@ -4,6 +4,7 @@ import cc.fascinated.common.MathUtils;
import cc.fascinated.model.score.TotalScoresResponse; import cc.fascinated.model.score.TotalScoresResponse;
import cc.fascinated.model.score.TrackedScore; import cc.fascinated.model.score.TrackedScore;
import cc.fascinated.model.token.ScoreSaberAccountToken; import cc.fascinated.model.token.ScoreSaberAccountToken;
import cc.fascinated.model.token.ScoreSaberLeaderboardPageToken;
import cc.fascinated.model.token.ScoreSaberLeaderboardToken; import cc.fascinated.model.token.ScoreSaberLeaderboardToken;
import cc.fascinated.model.user.User; import cc.fascinated.model.user.User;
import cc.fascinated.platform.CurvePoint; import cc.fascinated.platform.CurvePoint;
@ -18,6 +19,8 @@ import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -187,42 +190,53 @@ public class ScoreSaberPlatform extends Platform {
@Override @Override
public void updateLeaderboards() { public void updateLeaderboards() {
List<TrackedScore> scores = this.trackedScoreService.getTrackedScores(this.getPlatform(), false); // TODO: PUSH THIS
List<String> uniqueLeaderboardIds = scores.stream().map(TrackedScore::getLeaderboardId).distinct().toList(); List<TrackedScore> scores = this.trackedScoreService.getTrackedScores(this.getPlatform(), true);
Map<String, ScoreSaberLeaderboardToken> rankedLeaderboards = new HashMap<>();
for (ScoreSaberLeaderboardPageToken rankedLeaderboard : this.scoreSaberService.getRankedLeaderboards()) {
for (ScoreSaberLeaderboardToken leaderboard : rankedLeaderboard.getLeaderboards()) {
rankedLeaderboards.put(leaderboard.getId(), leaderboard);
}
}
log.info("Updating {} leaderboards for platform '{}'", log.info("Updating {} leaderboards for platform '{}'",
uniqueLeaderboardIds.size(), rankedLeaderboards.size(),
this.getPlatform().getPlatformName() this.getPlatform().getPlatformName()
); );
// Update the leaderboards // Update the leaderboards
int finished = 0; int finished = 0;
for (String id : uniqueLeaderboardIds) {
for (Map.Entry<String, ScoreSaberLeaderboardToken> leaderboardEntry : rankedLeaderboards.entrySet()) {
String id = leaderboardEntry.getKey();
ScoreSaberLeaderboardToken leaderboard = leaderboardEntry.getValue();
if (finished > 0) {
// Sleep to prevent rate limiting
try {
Thread.sleep(UPDATE_DELAY);
} catch (InterruptedException e) {
log.error("Failed to sleep for rate limit reset", e);
}
}
try { try {
ScoreSaberLeaderboardToken leaderboard = this.scoreSaberService.getLeaderboard(id, true); // Update the cached leaderboard
if (leaderboard == null) { if (leaderboard == null) {
log.warn("Failed to update leaderboard '{}' for platform '{}'", id, this.getPlatform().getPlatformName()); log.warn("Failed to update leaderboard '{}' for platform '{}'", id, this.getPlatform().getPlatformName());
continue; continue;
} }
if (leaderboard.getStars() == 0) { // Check if the leaderboard is ranked
log.warn("Leaderboard '{}' for platform '{}' is not ranked, skipping updating the score pp values",
id,
this.getPlatform().getPlatformName()
);
continue;
}
List<TrackedScore> toUpdate = scores.stream().filter(score -> { List<TrackedScore> toUpdate = scores.stream().filter(score -> {
if (!score.getLeaderboardId().equals(id)) { // Check if the leaderboard ID matches if (!score.getLeaderboardId().equals(id)) { // Check if the leaderboard ID matches
return false; return false;
} }
double oldPp = score.getPp() == null ? 0 : score.getPp();
double pp = this.getPp(leaderboard.getStars(), score.getAccuracy()); double pp = this.getPp(leaderboard.getStars(), score.getAccuracy());
log.info("Leaderboard: {}, Old PP: {}, New PP: {}", leaderboard.getSongName(), oldPp, pp); return pp != (score.getPp() == null ? 0D : score.getPp()); // Check if the pp has changed
return pp != oldPp; // Check if the pp has changed
}).toList(); }).toList();
for (TrackedScore score : toUpdate) { // Update the scores for (TrackedScore score : toUpdate) { // Update the scores
if (leaderboard.getStars() == 0) { // The leaderboard was unranked
score.setPp(0D);
}
double pp = this.getPp(leaderboard.getStars(), score.getAccuracy()); double pp = this.getPp(leaderboard.getStars(), score.getAccuracy());
score.setPp(pp); score.setPp(pp);
} }
@ -233,19 +247,13 @@ public class ScoreSaberPlatform extends Platform {
finished++; finished++;
log.info("Updated leaderboard '{}' for platform '{}', changed {} scores. ({}/{})", log.info("Updated leaderboard '{}' for platform '{}', changed {} scores. ({}/{})",
id, leaderboard.getSongName(),
this.getPlatform().getPlatformName(), this.getPlatform().getPlatformName(),
toUpdate.size(), toUpdate.size(),
finished,
uniqueLeaderboardIds.size()
);
// Sleep to prevent rate limiting finished,
try { rankedLeaderboards.size()
Thread.sleep(UPDATE_DELAY); );
} catch (InterruptedException e) {
log.error("Failed to sleep for rate limit reset", e);
}
} catch (Exception ex) { } catch (Exception ex) {
log.error("An error occurred while updating leaderboard '{}'", id, ex); log.error("An error occurred while updating leaderboard '{}'", id, ex);
} }

@ -120,12 +120,32 @@ public class PlatformService {
)); ));
log.info("Updating scores for platform '%s'...".formatted(platform.getPlatform().getPlatformName())); log.info("Updating scores for platform '%s'...".formatted(platform.getPlatform().getPlatformName()));
EXECUTOR_SERVICE.execute(platform::updateLeaderboards); // Update the leaderboards Document finalDocument = document;
} EXECUTOR_SERVICE.execute(() -> {
platform.updateLeaderboards();
// Update the document
finalDocument.put("currentCurveVersion", platform.getCurrentCurveVersion());
MongoService.INSTANCE.getPlatformsCollection().replaceOne(Filters.eq("_id", platform.getPlatform().getPlatformName()), finalDocument);
}); // Update the leaderboards
} else {
// Update the document // Update the document
document.put("currentCurveVersion", platform.getCurrentCurveVersion()); document.put("currentCurveVersion", platform.getCurrentCurveVersion());
MongoService.INSTANCE.getPlatformsCollection().replaceOne(Filters.eq("_id", platform.getPlatform().getPlatformName()), document); MongoService.INSTANCE.getPlatformsCollection().replaceOne(Filters.eq("_id", platform.getPlatform().getPlatformName()), document);
} }
} }
/**
* Gets the ScoreSaber platform.
*
* @return the ScoreSaber platform
*/
public ScoreSaberPlatform getScoreSaberPlatform() {
for (Platform platform : this.platforms) {
if (platform.getPlatform().getPlatformName().equalsIgnoreCase(Platform.Platforms.SCORESABER.getPlatformName())) {
return (ScoreSaberPlatform) platform;
}
}
return null;
}
}

@ -3,24 +3,32 @@ package cc.fascinated.services;
import cc.fascinated.common.Request; import cc.fascinated.common.Request;
import cc.fascinated.exception.impl.BadRequestException; import cc.fascinated.exception.impl.BadRequestException;
import cc.fascinated.model.token.ScoreSaberAccountToken; import cc.fascinated.model.token.ScoreSaberAccountToken;
import cc.fascinated.model.token.ScoreSaberLeaderboardPageToken;
import cc.fascinated.model.token.ScoreSaberLeaderboardToken; import cc.fascinated.model.token.ScoreSaberLeaderboardToken;
import cc.fascinated.model.user.User; import cc.fascinated.model.user.User;
import cc.fascinated.repository.mongo.ScoreSaberLeaderboardRepository; import cc.fascinated.repository.mongo.ScoreSaberLeaderboardRepository;
import kong.unirest.core.HttpResponse; import kong.unirest.core.HttpResponse;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional; import java.util.Optional;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
*/ */
@Service @Service
@Log4j2(topic = "ScoreSaber Service")
public class ScoreSaberService { public class ScoreSaberService {
private static final String SCORESABER_API = "https://scoresaber.com/api/"; private static final String SCORESABER_API = "https://scoresaber.com/api/";
private static final String GET_PLAYER_ENDPOINT = SCORESABER_API + "player/%s/full"; private static final String GET_PLAYER_ENDPOINT = SCORESABER_API + "player/%s/full";
private static final String GET_LEADERBOARD_ENDPOINT = SCORESABER_API + "leaderboard/by-id/%s/info"; 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";
/** /**
* The ScoreSaber leaderboard repository to use. * The ScoreSaber leaderboard repository to use.
@ -80,6 +88,47 @@ public class ScoreSaberService {
return leaderboard; return leaderboard;
} }
/**
* Gets a list of all the ranked leaderboards.
*
* @return the ranked leaderboards
* @throws BadRequestException if an error occurred while getting the leaderboards
*/
public List<ScoreSaberLeaderboardPageToken> getRankedLeaderboards() {
log.info("Getting all ranked leaderboards...");
List<ScoreSaberLeaderboardPageToken> pages = new LinkedList<>();
int page = 1;
do {
log.info("Getting ranked leaderboard page '%s'...".formatted(page));
ScoreSaberLeaderboardPageToken pageToken = getRankedLeaderboards(page);
pages.add(pageToken);
for (ScoreSaberLeaderboardToken leaderboard : pageToken.getLeaderboards()) {
this.leaderboardRepository.save(leaderboard);
}
page++;
} while (page <= ((pages.get(0).getMetadata().getTotal() / pages.get(0).getMetadata().getItemsPerPage()) + 1));
log.info("Finished getting all ranked leaderboards, found '{}' pages.", pages.size());
return pages;
}
/**
* Gets a list of the ranked leaderboards for a page.
*
* @param page the page to get the leaderboards for
* @return the ranked leaderboards
* @throws BadRequestException if an error occurred while getting the leaderboards
*/
public ScoreSaberLeaderboardPageToken getRankedLeaderboards(int page) {
HttpResponse<ScoreSaberLeaderboardPageToken> response = Request.get(GET_RANKED_LEADERBOARDS_ENDPOINT.formatted(page), ScoreSaberLeaderboardPageToken.class);
if (response.getParsingError().isPresent()) { // Failed to parse the response
throw new BadRequestException("Failed to parse ScoreSaber leaderboard page for page '%s'".formatted(page));
}
if (response.getStatus() != 200) { // The response was not successful
throw new BadRequestException("Failed to get ScoreSaber leaderboard page for page '%s'".formatted(page));
}
return response.getBody();
}
/** /**
* Gets a leaderboard for a leaderboard id. * Gets a leaderboard for a leaderboard id.
* *

@ -6,6 +6,8 @@ import cc.fascinated.model.token.ScoreSaberPlayerScoreToken;
import cc.fascinated.model.token.ScoreSaberScoreToken; import cc.fascinated.model.token.ScoreSaberScoreToken;
import cc.fascinated.model.token.ScoreSaberWebsocketDataToken; import cc.fascinated.model.token.ScoreSaberWebsocketDataToken;
import cc.fascinated.platform.Platform; import cc.fascinated.platform.Platform;
import cc.fascinated.platform.impl.ScoreSaberPlatform;
import cc.fascinated.services.PlatformService;
import cc.fascinated.services.QuestDBService; import cc.fascinated.services.QuestDBService;
import cc.fascinated.services.UserService; import cc.fascinated.services.UserService;
import cc.fascinated.websocket.Websocket; import cc.fascinated.websocket.Websocket;
@ -39,12 +41,19 @@ public class ScoreSaberWebsocket extends Websocket {
*/ */
private final QuestDBService questDBService; private final QuestDBService questDBService;
/**
* The platform service to use
*/
private final PlatformService platformService;
@Autowired @Autowired
public ScoreSaberWebsocket(@NonNull ObjectMapper objectMapper, @NonNull UserService userService, @NonNull QuestDBService questDBService) { public ScoreSaberWebsocket(@NonNull ObjectMapper objectMapper, @NonNull UserService userService, @NonNull QuestDBService questDBService,
@NonNull PlatformService platformService) {
super("ScoreSaber", "wss://scoresaber.com/ws"); super("ScoreSaber", "wss://scoresaber.com/ws");
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.userService = userService; this.userService = userService;
this.questDBService = questDBService; this.questDBService = questDBService;
this.platformService = platformService;
} }
@Override @SneakyThrows @Override @SneakyThrows
@ -71,6 +80,7 @@ public class ScoreSaberWebsocket extends Websocket {
double accuracy = ((double) score.getBaseScore() / leaderboard.getMaxScore()) * 100; double accuracy = ((double) score.getBaseScore() / leaderboard.getMaxScore()) * 100;
String difficulty = ScoreSaberUtils.parseDifficulty(leaderboard.getDifficulty().getDifficulty()); String difficulty = ScoreSaberUtils.parseDifficulty(leaderboard.getDifficulty().getDifficulty());
double pp = platformService.getScoreSaberPlatform().getPp(leaderboard.getStars(), accuracy); // Recalculate the PP
try (Sender sender = questDBService.getSender()) { try (Sender sender = questDBService.getSender()) {
sender.table("score") sender.table("score")
@ -80,7 +90,7 @@ public class ScoreSaberWebsocket extends Websocket {
// Score information // Score information
.symbol("leaderboard_id", leaderboard.getId()) .symbol("leaderboard_id", leaderboard.getId())
.symbol("score_id", score.getId()) .symbol("score_id", score.getId())
.doubleColumn("pp", score.getPp()) .doubleColumn("pp", pp)
.longColumn("rank", score.getRank()) .longColumn("rank", score.getRank())
.longColumn("score", score.getBaseScore()) .longColumn("score", score.getBaseScore())
.longColumn("modified_score", score.getModifiedScore()) .longColumn("modified_score", score.getModifiedScore())