26 Commits

Author SHA1 Message Date
3eceec72b3 Update dependency com.google.code.gson:gson to v2.11.0 2024-05-19 20:01:50 +00:00
Lee
9771b04589 Merge pull request 'Update dependency com.google.code.gson:gson to v2.10.1' (#1) from renovate/com.google.code.gson-gson-2.x into master
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m22s
Reviewed-on: #1
2024-04-28 02:11:49 +00:00
3a60a8050b Update dependency com.google.code.gson:gson to v2.10.1 2024-04-28 01:00:54 +00:00
8d1ef26183 add Sentry
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m31s
2024-04-28 01:07:26 +01:00
d96a38a996 don't save banned or inactive accounts
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m21s
2024-04-25 20:23:51 +01:00
6c63534988 Don't fetch new scores if the account is inactive or banned.
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m19s
2024-04-25 20:22:07 +01:00
f2f45ffa87 move it to another repo
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m22s
2024-04-25 20:12:58 +01:00
0b5f083366 don't load again if only page params changed
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Has been cancelled
2024-04-25 20:12:16 +01:00
ddaa1e7c97 fix double data loading
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m20s
2024-04-25 20:08:41 +01:00
c0d2781fd0 use account name not id
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m19s
2024-04-25 20:06:06 +01:00
e7ee0ef4af fix changing pages on the script
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Has been cancelled
2024-04-25 20:05:51 +01:00
80331ba972 maybe fix page switching?
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m19s
2024-04-25 19:56:06 +01:00
f5b8aa82c7 improve score fetching and add a curve version to scores
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m24s
2024-04-25 19:43:48 +01:00
e5fbb3d44c bump script version
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m19s
2024-04-25 08:32:15 +01:00
54b20cf016 make the script work when changing pages
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m20s
2024-04-25 08:30:50 +01:00
bc8a9f6fdc fix
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m21s
2024-04-25 08:15:19 +01:00
ae19233ddf return null bio if the raw bio is null
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m20s
2024-04-25 08:09:59 +01:00
2b8b135cf8 fix script failing to load on page load
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m19s
2024-04-25 07:34:22 +01:00
4cd6c27b2b joe
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m19s
2024-04-25 07:11:06 +01:00
b08961adf8 start work on the tampermonkey script 2024-04-25 07:11:05 +01:00
309a2211f4 don't bother with indexing for now until it becomes an issue
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m19s
2024-04-25 06:49:49 +01:00
9af387ca56 make index unique
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 1m1s
2024-04-25 06:42:26 +01:00
de572779e4 impl +1 raw per global pp
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m20s
2024-04-25 06:34:49 +01:00
23814d8cfd ci stuff
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m19s
2024-04-25 05:13:32 +01:00
cfc77b551c ci stuff
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 57s
2024-04-25 05:11:48 +01:00
d83535cbb9 ci stuff 2024-04-25 05:11:17 +01:00
15 changed files with 409 additions and 81 deletions

View File

@ -17,11 +17,11 @@ FROM eclipse-temurin:17.0.11_9-jre-focal
WORKDIR /home/container
# Copy the built jar file from the builder stage
COPY --from=builder /home/container/target/Paste-Backend.jar .
COPY --from=builder /home/container/target/ScoreSaberUtils-Backend.jar .
# Make port 3000 available to the world outside this container
EXPOSE 3000
ENV PORT=3000
# Make port 80 available to the world outside this container
EXPOSE 80
ENV PORT=80
# Run the jar file
CMD java -jar ScoreSaberUtils-Backend.jar -Djava.awt.headless=true

12
pom.xml
View File

@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>cc.fascinated</groupId>
<artifactId>ScoreSaberUtils</artifactId>
<artifactId>ScoreSaberUtils-Backend</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
@ -80,7 +80,15 @@
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.1</version>
<version>2.11.0</version>
</dependency>
<!-- Sentry -->
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-spring-boot-starter-jakarta</artifactId>
<version>7.8.0</version>
<scope>compile</scope>
</dependency>
<!-- Websockets -->

View File

@ -0,0 +1,31 @@
package cc.fascinated.backend.common;
import lombok.experimental.UtilityClass;
@UtilityClass
public class MathUtils {
/**
* Clamps a value between a minimum and maximum.
*
* @param value The value to clamp.
* @param min The minimum value.
* @param max The maximum value.
* @return The clamped value.
*/
public static double clamp(double value, double min, double max) {
return Math.max(min, Math.min(max, value));
}
/**
* Linearly interpolates between two values.
*
* @param a The first value.
* @param b The second value.
* @param t The interpolation value.
* @return The interpolated value.
*/
public static double lerp(double a, double b, double t) {
return a + t * (b - a);
}
}

View File

@ -1,5 +1,8 @@
package cc.fascinated.backend.common;
import lombok.experimental.UtilityClass;
@UtilityClass
public class Timer {
/**

View File

@ -2,6 +2,7 @@ package cc.fascinated.backend.exception;
import cc.fascinated.backend.model.response.ErrorResponse;
import io.micrometer.common.lang.NonNull;
import io.sentry.Sentry;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
@ -39,6 +40,7 @@ public final class ExceptionControllerAdvice {
}
if (status == null) { // Fallback to 500
status = HttpStatus.INTERNAL_SERVER_ERROR;
Sentry.captureException(ex); // Capture the exception
}
return new ResponseEntity<>(new ErrorResponse(status, message), status);
}

View File

@ -0,0 +1,22 @@
package cc.fascinated.backend.leaderboard;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor @Getter
public class Leaderboard {
/**
* The name of the leaderboard.
*/
private final String name;
/**
* The version of the leaderboard.
*/
private int curveVersion;
/**
* The curve of the leaderboard.
*/
private final LeaderboardCurvePoint[] curve;
}

View File

@ -0,0 +1,11 @@
package cc.fascinated.backend.leaderboard;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter @AllArgsConstructor
public class LeaderboardCurvePoint {
private final double a;
private final double b;
}

View File

@ -0,0 +1,164 @@
package cc.fascinated.backend.leaderboard.impl;
import cc.fascinated.backend.common.MathUtils;
import cc.fascinated.backend.leaderboard.Leaderboard;
import cc.fascinated.backend.leaderboard.LeaderboardCurvePoint;
import cc.fascinated.backend.model.score.Score;
import lombok.extern.log4j.Log4j2;
import java.util.ArrayList;
import java.util.List;
@Log4j2(topic = "ScoreSaber Leaderboard")
public class ScoreSaberLeaderboard extends Leaderboard {
public static final ScoreSaberLeaderboard INSTANCE = new ScoreSaberLeaderboard();
/**
* The base multiplier for stars.
*/
private final double starMultiplier = 42.11;
/**
* no idea, ngl
*/
private final double weightCoefficient = 0.965;
public ScoreSaberLeaderboard() {
super("ScoreSaber", 1, new LeaderboardCurvePoint[] {
new LeaderboardCurvePoint(1.0, 5.367394282890631),
new LeaderboardCurvePoint(0.9995, 5.019543595874787),
new LeaderboardCurvePoint(0.999, 4.715470646416203),
new LeaderboardCurvePoint(0.99825, 4.325027383589547),
new LeaderboardCurvePoint(0.9975, 3.996793606763322),
new LeaderboardCurvePoint(0.99625, 3.5526145337555373),
new LeaderboardCurvePoint(0.995, 3.2022017597337955),
new LeaderboardCurvePoint(0.99375, 2.9190155639254955),
new LeaderboardCurvePoint(0.9925, 2.685667856592722),
new LeaderboardCurvePoint(0.99125, 2.4902905794106913),
new LeaderboardCurvePoint(0.99, 2.324506282149922),
new LeaderboardCurvePoint(0.9875, 2.058947159052738),
new LeaderboardCurvePoint(0.985, 1.8563887693647105),
new LeaderboardCurvePoint(0.9825, 1.697536248647543),
new LeaderboardCurvePoint(0.98, 1.5702410055532239),
new LeaderboardCurvePoint(0.9775, 1.4664726399289512),
new LeaderboardCurvePoint(0.975, 1.3807102743105126),
new LeaderboardCurvePoint(0.9725, 1.3090333065057616),
new LeaderboardCurvePoint(0.97, 1.2485807759957321),
new LeaderboardCurvePoint(0.965, 1.1552120359501035),
new LeaderboardCurvePoint(0.96, 1.0871883573850478),
new LeaderboardCurvePoint(0.955, 1.0388633331418984),
new LeaderboardCurvePoint(0.95, 1.0),
new LeaderboardCurvePoint(0.94, 0.9417362980580238),
new LeaderboardCurvePoint(0.93, 0.9039994071865736),
new LeaderboardCurvePoint(0.92, 0.8728710341448851),
new LeaderboardCurvePoint(0.91, 0.8488375988124467),
new LeaderboardCurvePoint(0.9, 0.825756123560842),
new LeaderboardCurvePoint(0.875, 0.7816934560296046),
new LeaderboardCurvePoint(0.85, 0.7462290664143185),
new LeaderboardCurvePoint(0.825, 0.7150465663454271),
new LeaderboardCurvePoint(0.8, 0.6872268862950283),
new LeaderboardCurvePoint(0.75, 0.6451808210101443),
new LeaderboardCurvePoint(0.7, 0.6125565959114954),
new LeaderboardCurvePoint(0.65, 0.5866010012767576),
new LeaderboardCurvePoint(0.6, 0.18223233667439062),
new LeaderboardCurvePoint(0.0, 0.0)
});
}
/**
* Gets the modifier for the given accuracy.
*
* @param accuracy The accuracy.
* @return The modifier.
*/
public double getModifier(double accuracy) {
accuracy = MathUtils.clamp(accuracy, 0, 100) / 100;
LeaderboardCurvePoint prev = this.getCurve()[1];
for (LeaderboardCurvePoint point : this.getCurve()) {
if (point.getA() <= accuracy) {
double distance = (prev.getA() - accuracy) / (prev.getA() - point.getA());
return MathUtils.lerp(prev.getB(), point.getB(), distance);
}
prev = point;
}
return 0;
}
/**
* Gets the pp for the given accuracy and stars.
*
* @param accuracy The accuracy.
* @param stars The stars.
* @return The pp.
*/
public double getPP(double accuracy, double stars) {
double pp = stars * this.starMultiplier;
double modifier = this.getModifier(accuracy);
return modifier * pp;
}
/**
* Gets the total pp for the given scores.
*
* @param scores The scores.
* @return The total pp.
*/
private double getTotalPP(List<Score> scores, int startIdx) {
double totalPP = 0;
for (int i = 0; i < scores.size(); i++) {
totalPP += Math.pow(this.weightCoefficient, i + startIdx) * scores.get(i).getPp();
}
return totalPP;
}
/**
* Gets the pp at the given index for the given scores.
*
* @param bottomScores The scores.
* @param idx The index.
* @return The pp.
*/
private double getRawPPAtIdx(List<Score> bottomScores, int idx, double expected) {
double oldBottomPP = this.getTotalPP(bottomScores, idx);
double newBottomPP = this.getTotalPP(bottomScores, idx + 1);
return (expected + oldBottomPP - newBottomPP) / Math.pow(this.weightCoefficient, idx);
}
/**
* Gets the raw pp per global pp for the given scores.
*
* @param scores The scores.
* @param expectedPP The expected pp.
* @return The raw pp per global pp.
*/
public double getRawPerGlobalPP(List<Score> scores, double expectedPP) {
int left = 0;
int right = scores.size() - 1;
int boundaryIdx = -1;
while (left <= right) {
int mid = (left + right) / 2;
double bottomPP = this.getTotalPP(scores.subList(mid, scores.size()), mid);
List<Score> bottomSlice = new ArrayList<>(scores.subList(mid, scores.size()));
bottomSlice.add(0, scores.get(mid));
double modifiedBottomPP = this.getTotalPP(bottomSlice, mid);
double diff = modifiedBottomPP - bottomPP;
if (diff > expectedPP) {
boundaryIdx = mid;
left = mid + 1;
} else {
right = mid - 1;
}
}
if (boundaryIdx == -1) {
return this.getRawPPAtIdx(scores, 0, expectedPP);
} else {
return this.getRawPPAtIdx(scores.subList(boundaryIdx + 1, scores.size()), boundaryIdx + 1, expectedPP);
}
}
}

View File

@ -8,9 +8,14 @@ import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date;
/**
* An account for ScoreSaber.
*/
@Document
@AllArgsConstructor
@Getter @Setter
public class Account {
@ -45,6 +50,11 @@ public class Account {
*/
private double performancePoints;
/**
* The amount of raw pp to get 1 global performance point.
*/
private double rawPerGlobalPerformancePoints;
/**
* The rank for this account.
*/
@ -117,6 +127,7 @@ public class Account {
Bio.fromRaw(token.getBio()),
token.getCountry(),
token.getPp(),
-1,
token.getRank(),
token.getCountryRank(),
token.getRole(),
@ -152,6 +163,9 @@ public class Account {
* @return The bio.
*/
public static Bio fromRaw(String raw) {
if (raw == null || raw.isEmpty()) {
return null;
}
return new Bio(
raw.split("\n"),
raw.replaceAll("<[^>]*>", "").split("\n")

View File

@ -5,12 +5,14 @@ import cc.fascinated.backend.model.score.Score;
import cc.fascinated.backend.model.token.ScoreSaberLeaderboardToken;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date;
/**
* A leaderboard for a song.
*/
@Document
@AllArgsConstructor @Getter
public class Leaderboard {
/**

View File

@ -1,6 +1,7 @@
package cc.fascinated.backend.model.score;
import cc.fascinated.backend.common.DateUtils;
import cc.fascinated.backend.leaderboard.impl.ScoreSaberLeaderboard;
import cc.fascinated.backend.model.token.ScoreSaberLeaderboardToken;
import cc.fascinated.backend.model.token.ScoreSaberPlayerScoreToken;
import cc.fascinated.backend.model.token.ScoreSaberScoreToken;
@ -8,12 +9,17 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@AllArgsConstructor @Getter @Setter
/**
* A score for an account.
*/
@Document
@Getter @Setter
public class Score {
/**
* The id for this score.
@ -39,7 +45,7 @@ public class Score {
/**
* The PP for this score.
*/
private double pp;
private Double pp;
/**
* The weight for this score.
@ -121,11 +127,45 @@ public class Score {
*/
private String leaderboardId;
/**
* The curve version for this score.
*/
private Integer curveVersion;
/**
* The difficulty this score was set on.
*/
private Difficulty difficulty;
public Score(String id, int rank, int baseScore, int modifiedScore, Double pp, int weight, List<String> modifiers,
int multiplier, int badCuts, int missedNotes, int maxCombo, boolean fullCombo, int hmd, Date timeSet,
boolean hasReplay, String deviceHmd, String deviceControllerLeft, String deviceControllerRight,
List<Score> previousScores, String accountId, String leaderboardId, Integer curveVersion, Difficulty difficulty) {
this.id = id;
this.rank = rank;
this.baseScore = baseScore;
this.modifiedScore = modifiedScore;
this.pp = pp;
this.weight = weight;
this.modifiers = modifiers;
this.multiplier = multiplier;
this.badCuts = badCuts;
this.missedNotes = missedNotes;
this.maxCombo = maxCombo;
this.fullCombo = fullCombo;
this.hmd = hmd;
this.timeSet = timeSet;
this.hasReplay = hasReplay;
this.deviceHmd = deviceHmd;
this.deviceControllerLeft = deviceControllerLeft;
this.deviceControllerRight = deviceControllerRight;
this.previousScores = previousScores;
this.accountId = accountId;
this.leaderboardId = leaderboardId;
this.curveVersion = curveVersion == null ? ScoreSaberLeaderboard.INSTANCE.getCurveVersion() : curveVersion;
this.difficulty = difficulty;
}
/**
* Gets a score from the given token.
*
@ -164,6 +204,7 @@ public class Score {
new ArrayList<>(),
playerId,
leaderboard.getId(),
ScoreSaberLeaderboard.INSTANCE.getCurveVersion(), // Get the curve version from the leaderboard.
Difficulty.fromId(leaderboard.getDifficulty().getDifficulty())
);
}

View File

@ -18,7 +18,16 @@ public interface ScoreRepository extends MongoRepository<Score, String> {
* @return The scores for the account.
*/
@Query("{ 'accountId' : ?0 }")
List<Score> getScoresForAccount(String accountId);
List<Score> getScores(String accountId);
/**
* Gets the ranked scores for an account.
*
* @param accountId The id of the account.
* @return The ranked scores for the account.
*/
@Query(value = "{ 'accountId' : ?0, 'pp' : { $gt : 0 } }", sort = "{ 'pp' : -1 }")
List<Score> getRankedScores(String accountId);
/**
* Gets the scores sorted by the newest for an account.
@ -28,4 +37,14 @@ public interface ScoreRepository extends MongoRepository<Score, String> {
*/
@Query(value = "{ 'accountId' : ?0 }", sort = "{ 'timeSet' : -1 }")
List<Score> getScoresSortedByNewest(String accountId);
/**
* Gets the scores for an account and a leaderboard.
*
* @param accountId The id of the account.
* @param leaderboardId The id of the leaderboard.
* @return The scores for the leaderboard.
*/
@Query(value = "{ 'accountId' : ?0, 'leaderboardId' : ?1 }", sort = "{ 'timeSet' : -1 }")
List<Score> getScoreForLeaderboard(String accountId, String leaderboardId);
}

View File

@ -1,9 +1,11 @@
package cc.fascinated.backend.service;
import cc.fascinated.backend.common.Timer;
import cc.fascinated.backend.leaderboard.impl.ScoreSaberLeaderboard;
import cc.fascinated.backend.model.account.Account;
import cc.fascinated.backend.model.token.ScoreSaberAccountToken;
import cc.fascinated.backend.repository.AccountRepository;
import cc.fascinated.backend.repository.ScoreRepository;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -25,14 +27,20 @@ public class AccountService {
*/
private final AccountRepository accountRepository;
/**
* The {@link ScoreRepository} instance.
*/
private final ScoreRepository scoreRepository;
/**
* The {@link ScoreSaberService} instance.
*/
private final ScoreSaberService scoreSaberService;
@Autowired
public AccountService(AccountRepository accountRepository, ScoreSaberService scoreSaberService) {
public AccountService(AccountRepository accountRepository, ScoreRepository scoreRepository, ScoreSaberService scoreSaberService) {
this.accountRepository = accountRepository;
this.scoreRepository = scoreRepository;
this.scoreSaberService = scoreSaberService;
Timer.scheduleRepeating(() -> {
@ -62,11 +70,7 @@ public class AccountService {
Optional<Account> optionalAccount = accountRepository.findById(id);
if (optionalAccount.isEmpty()) {
log.info("Account '{}' not found in the database. Fetching from ScoreSaber API.", id);
Account account = Account.fromToken(scoreSaberService.getAccount(id));
updateAccount(account); // Fetch the scores for the account.
accountRepository.save(account); // Save the account to the database.
return account;
return updateAccount(Account.fromToken(scoreSaberService.getAccount(id)));
}
log.info("Account '{}' found in the database.", id);
return optionalAccount.get();
@ -78,21 +82,31 @@ public class AccountService {
*
* @param account The account.
*/
public void updateAccount(Account account) {
public Account updateAccount(Account account) {
String id = account.getId();
// Fetch the account from the ScoreSaber API.
ScoreSaberAccountToken accountToken = scoreSaberService.getAccount(id); // Fetch the account from the ScoreSaber API.
if (accountToken == null) {
log.warn("Account '{}' not found in the ScoreSaber API.", id);
return;
return null;
}
// Update the account with the new token.
Account updatedAccount = Account.fromToken(accountToken);
account = accountRepository.save(updatedAccount); // Save the account to the database.
account = Account.fromToken(accountToken);
accountRepository.save(account);
// Don't fetch new scores if the account is inactive or banned.
if (account.isInactive() || account.isBanned()) {
return account;
}
// Fetch the scores for the account.
scoreSaberService.updateScores(account);
// Set the raw pp per +1 global pp
double rawPerGlobalPP = ScoreSaberLeaderboard.INSTANCE.getRawPerGlobalPP(scoreRepository.getRankedScores(id), 1);
account.setRawPerGlobalPerformancePoints(rawPerGlobalPP);
return accountRepository.save(account);
}
}

View File

@ -4,6 +4,7 @@ import cc.fascinated.backend.Main;
import cc.fascinated.backend.common.DateUtils;
import cc.fascinated.backend.common.Timer;
import cc.fascinated.backend.common.WebRequest;
import cc.fascinated.backend.exception.impl.BadRequestException;
import cc.fascinated.backend.exception.impl.RateLimitException;
import cc.fascinated.backend.exception.impl.ResourceNotFoundException;
import cc.fascinated.backend.model.account.Account;
@ -76,8 +77,15 @@ public class ScoreSaberService extends TextWebSocketHandler {
public ScoreSaberAccountToken getAccount(String id) {
ScoreSaberAccountToken account = WebRequest.getAsEntity(String.format(GET_PLAYER_ENDPOINT, id), ScoreSaberAccountToken.class);
if (account == null) { // Check if the account doesn't exist.
log.info("Account with id '{}' not found.", id);
throw new ResourceNotFoundException("Account with id '%s' not found.".formatted(id));
}
if (account.isBanned()) {
throw new BadRequestException("Account with id '%s' is banned.".formatted(id));
}
if (account.isInactive()) {
throw new BadRequestException("Account with id '%s' is inactive.".formatted(id));
}
return account;
}
@ -89,7 +97,7 @@ public class ScoreSaberService extends TextWebSocketHandler {
* @return The scores.
*/
public ScoreSaberScoresPageToken getPageScores(Account account, int page) {
log.info("Fetching scores for account '{}' from page {}.", account.getId(), page);
log.info("Fetching scores for account '{}' from page {}.", account.getName(), page);
ScoreSaberScoresPageToken pageToken = WebRequest.getAsEntity(String.format(GET_PLAYER_SCORES_ENDPOINT, account.getId(), "recent", page), ScoreSaberScoresPageToken.class);
if (pageToken == null) { // Check if the page doesn't exist.
return null;
@ -119,54 +127,29 @@ public class ScoreSaberService extends TextWebSocketHandler {
return scores;
}
/**
* Fetch the scores until the specified score id.
*
* @param account The account.
* @param scoreUntil The score to fetch until.
* @return The scores.
*/
public List<ScoreSaberPlayerScoreToken> getScoreUntil(Account account, Score scoreUntil) {
List<ScoreSaberPlayerScoreToken> scores = new ArrayList<>();
int page = 1;
do {
ScoreSaberScoresPageToken pageToken = getPageScores(account, page);
for (ScoreSaberPlayerScoreToken score : pageToken.getPlayerScores()) {
// If the score isn't the same as the scoreUntil, add it to the list.
if (!DateUtils.getDateFromString(score.getScore().getTimeSet()).equals(scoreUntil.getTimeSet())) {
scores.add(score);
}
if (score.getScore().getId().equals(scoreUntil.getId())) {
// If the current score matches the specified scoreUntil, stop fetching.
return scores;
}
}
page++;
} while (true);
}
/**
* Fetches the scores for the account.
*
* @param account The account.
*/
public void updateScores(Account account) {
String id = account.getId();
String name = account.getName();
// Fetch the scores for the account.
List<Score> scores = scoreRepository.getScoresForAccount(id);
List<Score> scores = scoreRepository.getScores(account.getId());
if (scores.isEmpty()) {
log.warn("Account '{}' has no scores, fetching them.", id);
log.warn("Account '{}' has no scores, fetching them.", name);
List<ScoreSaberScoresPageToken> scoresPageTokens = this.getScores(account);
List<Score> newScores = new ArrayList<>();
List<Leaderboard> leaderboardToSave = new ArrayList<>();
for (ScoreSaberScoresPageToken page : scoresPageTokens) {
for (ScoreSaberPlayerScoreToken score : page.getPlayerScores()) {
newScores.add(Score.fromToken(id, score));
leaderboardToSave.add(Leaderboard.fromToken(score.getLeaderboard()));
for (ScoreSaberPlayerScoreToken scoreToken : page.getPlayerScores()) {
Score score = Score.fromToken(account.getId(), scoreToken);
newScores.add(score);
scores.add(score);
leaderboardToSave.add(Leaderboard.fromToken(scoreToken.getLeaderboard()));
}
}
@ -178,39 +161,45 @@ public class ScoreSaberService extends TextWebSocketHandler {
}
scoreRepository.saveAll(newScores); // Save the player's scores.
log.info("Found {} scores for account '{}'.", newScores.size(), id);
log.info("Found {} scores for account '{}'.", newScores.size(), name);
return;
}
long start = System.currentTimeMillis();
log.info("Fetching new scores for account '{}'.", id);
Score latestScore = scoreRepository.getScoresSortedByNewest(id).get(0);
log.info("Fetching new scores for account '{}'.", name);
List<ScoreSaberPlayerScoreToken> newScores = this.getScoreUntil(account, latestScore);
if (newScores.isEmpty()) {
log.info("No new scores found for account '{}'.", id);
return;
}
int newScoreCount = 0;
for (ScoreSaberPlayerScoreToken newScore : newScores) {
if (saveScore(account, newScore)) {
newScoreCount++;
int page = 1; // The current page to search for scores.
boolean done = false; // Whether we are done fetching scores.
List<ScoreSaberPlayerScoreToken> newScores = new ArrayList<>();
do {
// This will keep fetching score pages until it finds a score that already exists.
ScoreSaberScoresPageToken pageScores = getPageScores(account, page);
for (ScoreSaberPlayerScoreToken score : pageScores.getPlayerScores()) {
boolean exists = scores.stream().anyMatch(s -> s.getId().equals(score.getScore().getId()));
if (!exists) {
newScores.add(score);
continue;
}
done = true;
}
page++;
} while (!done);
// Save the new scores.
for (ScoreSaberPlayerScoreToken score : newScores) {
saveScore(account, score);
}
log.info("Found {} new scores for account '{}'. (took: {}ms)", newScoreCount, id, System.currentTimeMillis() - start);
log.info("Found {} new scores for account '{}'. (took: {}ms)", newScores.size(), name, System.currentTimeMillis() - start);
}
/**
* Saves the score for the account.
*
* @param account The account.
* @param score The score to save.
* @return Whether the score was saved.
* @param score The score to save.
*/
private boolean saveScore(Account account, ScoreSaberPlayerScoreToken score) {
boolean didSave = false;
private void saveScore(Account account, ScoreSaberPlayerScoreToken score) {
Leaderboard newScoreLeaderboard = Leaderboard.fromToken(score.getLeaderboard());
Score oldScore = scoreRepository.findById(score.getScore().getId()).orElse(null);
@ -226,19 +215,16 @@ public class ScoreSaberService extends TextWebSocketHandler {
scoreRepository.delete(oldScore); // Delete the old score.
scoreRepository.save(scoreSet); // Save the new score.
didSave = true;
}
} else {
// The score is new
scoreRepository.save(Score.fromToken(account.getId(), score)); // Save the new score.
didSave = true;
}
// Check if the leaderboard doesn't already exist.
if (leaderboardRepository.findById(newScoreLeaderboard.getId()).isEmpty()) {
leaderboardRepository.save(newScoreLeaderboard); // Save the leaderboard.
}
return didSave;
}
/**
@ -275,9 +261,21 @@ public class ScoreSaberService extends TextWebSocketHandler {
*/
@SneakyThrows
private void connectWebSocket() {
log.info("Connecting to the ScoreSaber WSS.");
new StandardWebSocketClient().execute(this, "wss://scoresaber.com/ws").get();
}
@Override
public void afterConnectionEstablished(@NonNull WebSocketSession session) {
log.info("Connected to the ScoreSaber WSS.");
}
@Override
public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus status) {
log.info("Disconnected from the ScoreSaber WSS.");
connectWebSocket(); // Reconnect to the WebSocket.
}
@Override @SneakyThrows
protected void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) {
// Ignore the connection message.
@ -309,10 +307,4 @@ public class ScoreSaberService extends TextWebSocketHandler {
log.error("An error occurred while handling the message.", ex);
}
}
@Override
public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus status) {
log.info("Disconnected from the ScoreSaber WSS.");
connectWebSocket(); // Reconnect to the WebSocket.
}
}

View File

@ -1,6 +1,6 @@
server:
address: 0.0.0.0
port: 3005
port: 80
servlet:
context-path: /
@ -15,6 +15,11 @@ spring:
uri: mongodb://localhost:27017
database: ssu-prod
# Sentry Configuration
sentry:
dsn: ""
tracesSampleRate: 1.0
# Set the embedded MongoDB version
de:
flapdoodle: