store the users scoresaber profile and add more data to the top scores endpoint

This commit is contained in:
Lee 2024-08-01 16:19:27 +01:00
parent 0f7a890e44
commit 242a8a2fba
19 changed files with 615 additions and 42 deletions

@ -0,0 +1,26 @@
package cc.fascinated.common;
import lombok.experimental.UtilityClass;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Date;
/**
* @author Fascinated (fascinated7)
*/
@UtilityClass
public class DateUtils {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_INSTANT;
/**
* Gets the date from a string.
*
* @param date The date string.
* @return The date.
*/
public static Date getDateFromString(String date) {
return Date.from(Instant.from(FORMATTER.parse(date)));
}
}

@ -2,6 +2,9 @@ package cc.fascinated.common;
import lombok.experimental.UtilityClass;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
/**
* @author Fascinated (fascinated7)
*/
@ -31,4 +34,19 @@ public class MathUtils {
public static double lerp(double a, double b, double t) {
return a + t * (b - a);
}
/**
* Format a number to a specific amount of decimal places.
*
* @param number the number to format
* @param additional the additional decimal places to format
* @return the formatted number
*/
public static double format(double number, int additional) {
return Double.parseDouble(
new DecimalFormat("#.#" + "#".repeat(Math.max(0, additional - 1)),
new DecimalFormatSymbols()
).format(number)
);
}
}

@ -0,0 +1,166 @@
package cc.fascinated.common;
import lombok.*;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Fascinated (fascinated7)
*/
@UtilityClass
public final class TimeUtils {
/**
* Format a time in millis to a readable time format.
*
* @param millis the millis to format
* @return the formatted time
*/
public static String format(long millis) {
return format(millis, BatTimeFormat.FIT);
}
/**
* Format a time in millis to a readable time format.
*
* @param millis the millis to format
* @param timeUnit the time unit to format the millis to
* @return the formatted time
*/
public static String format(long millis, BatTimeFormat timeUnit) {
return format(millis, timeUnit, false);
}
/**
* Format a time in millis to a readable time format.
*
* @param millis the millis to format
* @param timeUnit the time unit to format the millis to
* @param compact whether to use a compact display
* @return the formatted time
*/
public static String format(long millis, BatTimeFormat timeUnit, boolean compact) {
return format(millis, timeUnit, true, compact);
}
/**
* Format a time in millis to a readable time format.
*
* @param millis the millis to format
* @param timeUnit the time unit to format the millis to
* @param decimals whether to include decimals
* @param compact whether to use a compact display
* @return the formatted time
*/
public static String format(long millis, BatTimeFormat timeUnit, boolean decimals, boolean compact) {
if (millis == -1L) { // Format permanent
return "Perm" + (compact ? "" : "anent");
}
// Format the time to the best fitting time unit
if (timeUnit == BatTimeFormat.FIT) {
for (BatTimeFormat otherTimeUnit : BatTimeFormat.VALUES) {
if (otherTimeUnit != BatTimeFormat.FIT && millis >= otherTimeUnit.getMillis()) {
timeUnit = otherTimeUnit;
break;
}
}
}
double time = MathUtils.format((double) millis / timeUnit.getMillis(), 1); // Format the time
if (!decimals) { // Remove decimals
time = (int) time;
}
String formatted = time + (compact ? timeUnit.getSuffix() : " " + timeUnit.getDisplay()); // Append the time unit
if (time != 1.0 && !compact) { // Pluralize the time unit
formatted += "s";
}
return formatted;
}
/**
* Convert the given input into a time in millis.
* <p>
* E.g: 1d, 1h, 1d1h, etc
* </p>
*
* @param input the input to parse
* @return the time in millis
*/
public static long fromString(String input) {
Matcher matcher = BatTimeFormat.SUFFIX_PATTERN.matcher(input); // Match the given input
long millis = 0; // The total millis
// Match corresponding suffixes and add up the total millis
while (matcher.find()) {
int amount = Integer.parseInt(matcher.group(1)); // The amount of time to add
String suffix = matcher.group(2); // The unit suffix
BatTimeFormat timeUnit = BatTimeFormat.fromSuffix(suffix); // The time unit to add
if (timeUnit != null) { // Increment the total millis
millis += amount * timeUnit.getMillis();
}
}
return millis;
}
/**
* Represents a unit of time.
*/
@NoArgsConstructor
@AllArgsConstructor
@Getter(AccessLevel.PRIVATE)
@ToString
public enum BatTimeFormat {
FIT,
YEARS("Year", "y", TimeUnit.DAYS.toMillis(365L)),
MONTHS("Month", "mo", TimeUnit.DAYS.toMillis(30L)),
WEEKS("Week", "w", TimeUnit.DAYS.toMillis(7L)),
DAYS("Day", "d", TimeUnit.DAYS.toMillis(1L)),
HOURS("Hour", "h", TimeUnit.HOURS.toMillis(1L)),
MINUTES("Minute", "m", TimeUnit.MINUTES.toMillis(1L)),
SECONDS("Second", "s", TimeUnit.SECONDS.toMillis(1L)),
MILLISECONDS("Millisecond", "ms", 1L);
/**
* Our cached unit values.
*/
public static final BatTimeFormat[] VALUES = values();
/**
* Our cached suffix pattern.
*/
public static final Pattern SUFFIX_PATTERN = Pattern.compile("(\\d+)(mo|ms|[ywdhms])");
/**
* The display of this time unit.
*/
private String display;
/**
* The suffix of this time unit.
*/
private String suffix;
/**
* The amount of millis in this time unit.
*/
private long millis;
/**
* Get the time unit with the given suffix.
*
* @param suffix the time unit suffix
* @return the time unit, null if not found
*/
@Nullable
public static BatTimeFormat fromSuffix(String suffix) {
for (BatTimeFormat unit : VALUES) {
if (unit != FIT && unit.getSuffix().equals(suffix)) {
return unit;
}
}
return null;
}
}
}

@ -1,13 +1,13 @@
package cc.fascinated.controller;
import java.util.Map;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* @author Fascinated (fascinated7)
*/

@ -38,7 +38,7 @@ public class ScoresController {
@ResponseBody
@GetMapping(value = "/top/{platform}")
public ResponseEntity<List<?>> getTopScores(@PathVariable String platform) {
return ResponseEntity.ok(trackedScoreService.getTopScores(Platform.Platforms.getPlatform(platform), 100));
return ResponseEntity.ok(trackedScoreService.getTopScores(Platform.Platforms.getPlatform(platform), 50));
}
/**

@ -1,7 +1,6 @@
package cc.fascinated.controller;
import cc.fascinated.exception.impl.BadRequestException;
import cc.fascinated.model.user.User;
import cc.fascinated.model.user.UserDTO;
import cc.fascinated.services.UserService;
import lombok.NonNull;

@ -0,0 +1,21 @@
package cc.fascinated.model.leaderboard;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
public class Difficulty {
/**
* The difficulty of the song.
*/
private String difficulty;
/**
* The raw difficulty of the song.
*/
private String difficultyRaw;
}

@ -0,0 +1,81 @@
package cc.fascinated.model.leaderboard;
import cc.fascinated.common.ScoreSaberUtils;
import cc.fascinated.model.token.ScoreSaberLeaderboardToken;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
public class Leaderboard {
/**
* The ID of the leaderboard.
*/
private String id;
/**
* The hash of the song.
*/
private String songHash;
/**
* The name of the song.
*/
private String songName;
/**
* The sub name of the song.
*/
private String songSubName;
/**
* The author of the song.
*/
private String songAuthorName;
/**
* The mapper of the song.
*/
private String levelAuthorName;
/**
* The difficulty of the song.
*/
private Difficulty difficulty;
/**
* The star rating for this leaderboard.
*/
private double stars;
/**
* The cover image for this leaderboard.
*/
private String coverImage;
/**
* Constructs a new {@link Leaderboard} object
* from a {@link ScoreSaberLeaderboardToken} object.
*
* @param token The token to construct the object from.
* @return The leaderboard.
*/
public static Leaderboard getFromScoreSaberToken(ScoreSaberLeaderboardToken token) {
return new Leaderboard(
token.getId(),
token.getSongHash(),
token.getSongName(),
token.getSongSubName(),
token.getSongAuthorName(),
token.getLevelAuthorName(),
new Difficulty(
ScoreSaberUtils.parseDifficulty(token.getDifficulty().getDifficulty()),
token.getDifficulty().getDifficultyRaw()
),
token.getStars(),
token.getCoverImage()
);
}
}

@ -0,0 +1,95 @@
package cc.fascinated.model.score;
import cc.fascinated.model.leaderboard.Leaderboard;
import cc.fascinated.model.user.ScoreSaberAccount;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Date;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
public class ScoreResponse {
/**
* The ID of the score.
*/
private String scoreId;
/**
* The ID of the player who set the score.
*/
private ScoreSaberAccount player;
/**
* The ID of the leaderboard.
*/
private Leaderboard leaderboard;
/**
* The PP of the score.
*/
private Double pp;
/**
* The rank of the score.
*/
private Long rank;
/**
* The base score of the score.
*/
private Long score;
/**
* The modified score of the score.
*/
private Long modifiedScore;
/**
* The weight of the score.
*/
private Double weight;
/**
* The modifiers of the score.
*/
private String modifiers;
/**
* The multiplier of the score.
*/
private double multiplier;
/**
* The number of misses in the score.
*/
private Long missedNotes;
/**
* The number of bad cuts in the score.
*/
private Long badCuts;
/**
* The highest combo in the score.
*/
private Long maxCombo;
/**
* The accuracy of the score.
*/
private Double accuracy;
/**
* The difficulty the score was set on.
*/
private String difficulty;
/**
* The timestamp of the score.
*/
private Date timestamp;
}

@ -0,0 +1,63 @@
package cc.fascinated.model.user;
import cc.fascinated.common.DateUtils;
import cc.fascinated.model.token.ScoreSaberAccountToken;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Date;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
public class ScoreSaberAccount {
/**
* The avatar of the user.
*/
private String avatar;
/**
* The country of the user.
*/
private String country;
/**
* The rank of the user.
*/
private int rank;
/**
* The country rank of the user.
*/
private int countryRank;
/**
* The date the user joined ScoreSaber.
*/
private Date accountCreated;
/**
* The date the user was last updated.
*/
private Date lastUpdated;
/**
* Constructs a new {@link ScoreSaberAccount} object
* from a {@link ScoreSaberAccountToken} object.
*
* @param token The token to construct the object from.
* @return The scoresaber account.
*/
public static ScoreSaberAccount getFromToken(ScoreSaberAccountToken token) {
return new ScoreSaberAccount(
token.getProfilePicture(),
token.getCountry(),
token.getRank(),
token.getCountryRank(),
DateUtils.getDateFromString(token.getFirstSeen()),
new Date()
);
}
}

@ -5,6 +5,8 @@ import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.UUID;
@ -15,6 +17,7 @@ import java.util.UUID;
@Getter
@Setter
@ToString
@Document("user")
public class User {
/**
* The ID of the user.
@ -28,11 +31,13 @@ public class User {
* Usually their Steam name.
* </p>
*/
@Indexed
private String username;
/**
* The ID of the users steam profile.
*/
@Indexed(unique = true)
private String steamId;
/**
@ -44,6 +49,11 @@ public class User {
*/
public boolean hasLoggedIn;
/**
* The user's ScoreSaber account token.
*/
public ScoreSaberAccount scoresaberAccount;
/**
* Converts the User object to a UserDTO object.
*

@ -69,20 +69,19 @@ public abstract class Platform {
public abstract double getPp(double stars, double accuracy);
/**
* Called every 10 minutes to update
* the players data in QuestDB.
* Called to update the players
* data in QuestDB.
*/
public abstract void updatePlayers();
/**
* Called every 10 minutes to update
* the metrics for total scores, etc.
* Called to update the metrics
* for total scores, etc.
*/
public abstract void updateMetrics();
/**
* Called every day at midnight to update
* the leaderboards.
* Called to update the leaderboards.
*/
public abstract void updateLeaderboards();

@ -19,7 +19,6 @@ import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -153,7 +152,7 @@ public class ScoreSaberPlatform extends Platform {
@Override
public void updatePlayers() {
for (User user : this.userService.getUsers()) {
for (User user : this.userService.getUsers(false)) {
if (!user.isHasLoggedIn()) { // Check if the user has linked their account
continue;
}

@ -2,7 +2,9 @@ package cc.fascinated.repository.mongo;
import cc.fascinated.model.user.User;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@ -17,4 +19,21 @@ public interface UserRepository extends MongoRepository<User, UUID> {
* @return the user
*/
Optional<User> findBySteamId(String steamId);
/**
* Fetches all users and only their steam ids.
*
* @return the list of users
*/
@Query(value = "{}", fields = "{ 'steamId' : 1 }")
List<User> fetchOnlySteamIds();
/**
* Finds a user by their username.
*
* @param username the username of the user
* @return the user
*/
@Query("{ $text: { $search: ?0 } }")
List<User> findUsersByUsername(String username);
}

@ -2,7 +2,6 @@ package cc.fascinated.services;
import cc.fascinated.platform.Platform;
import cc.fascinated.platform.impl.ScoreSaberPlatform;
import cc.fascinated.repository.couchdb.TrackedScoreRepository;
import com.mongodb.client.model.Filters;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
@ -59,12 +58,12 @@ public class PlatformService {
* </p>
*/
@Scheduled(cron = "0 */15 * * * *")
public void updateScores() {
log.info("Updating %s platform players...".formatted(this.platforms.size()));
public void updatePlayerMetrics() {
log.info("Updating %s platform player metrics...".formatted(this.platforms.size()));
for (Platform platform : this.platforms) {
platform.updatePlayers();
}
log.info("Finished updating platform players.");
log.info("Finished updating platform player metrics.");
}
/**

@ -10,8 +10,6 @@ import cc.fascinated.repository.mongo.ScoreSaberLeaderboardRepository;
import kong.unirest.core.HttpResponse;
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.stereotype.Service;
@ -50,15 +48,15 @@ public class ScoreSaberService {
*/
public ScoreSaberAccountToken getAccount(User user) {
if (user.getSteamId() == null) {
throw new BadRequestException("%s does not have a linked ScoreSaber account".formatted(user.getUsername()));
throw new BadRequestException("The user does not have a steam id");
}
HttpResponse<ScoreSaberAccountToken> 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()));
throw new BadRequestException("Failed to parse ScoreSaber account for '%s'".formatted(user.getSteamId()));
}
if (response.getStatus() != 200) { // The response was not successful
throw new BadRequestException("Failed to get ScoreSaber account for '%s'".formatted(user.getUsername()));
throw new BadRequestException("Failed to get ScoreSaber account for '%s'".formatted(user.getSteamId()));
}
return response.getBody();
}

@ -1,15 +1,19 @@
package cc.fascinated.services;
import cc.fascinated.exception.impl.BadRequestException;
import cc.fascinated.model.leaderboard.Leaderboard;
import cc.fascinated.model.score.ScoreResponse;
import cc.fascinated.model.score.ScoresOverResponse;
import cc.fascinated.model.score.TotalScoresResponse;
import cc.fascinated.model.score.TrackedScore;
import cc.fascinated.model.user.User;
import cc.fascinated.platform.Platform;
import cc.fascinated.repository.couchdb.TrackedScoreRepository;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.LinkedList;
import java.util.List;
/**
@ -28,9 +32,24 @@ public class TrackedScoreService {
@NonNull
private final TrackedScoreRepository trackedScoreRepository;
/**
* The user service to use.
*/
@NonNull
private final UserService userService;
/**
* The ScoreSaber service to use.
*/
@NonNull
private final ScoreSaberService scoreSaberService;
@Autowired
public TrackedScoreService(@NonNull TrackedScoreRepository trackedScoreRepository) {
public TrackedScoreService(@NonNull TrackedScoreRepository trackedScoreRepository, @NonNull UserService userService,
@NonNull ScoreSaberService scoreSaberService) {
this.trackedScoreRepository = trackedScoreRepository;
this.userService = userService;
this.scoreSaberService = scoreSaberService;
this.trackedScoreRepository.ensureDeduplication();
}
@ -42,11 +61,41 @@ public class TrackedScoreService {
* @param amount the amount of scores to get
* @return the scores
*/
public List<TrackedScore> getTopScores(Platform.Platforms platform, int amount) {
List<TrackedScore> scores = trackedScoreRepository.findTopRankedScores(platform.getPlatformName(), amount);
if (scores.isEmpty()) {
public List<ScoreResponse> getTopScores(Platform.Platforms platform, int amount) {
List<TrackedScore> foundScores = trackedScoreRepository.findTopRankedScores(platform.getPlatformName(), amount);
if (foundScores.isEmpty()) {
throw new BadRequestException("No scores found for platform " + platform.getPlatformName());
}
List<ScoreResponse> scores = new LinkedList<>();
for (TrackedScore trackedScore : foundScores) {
User user = this.userService.getUser(trackedScore.getPlayerId());
Leaderboard leaderboard = null;
switch (platform) {
case SCORESABER -> leaderboard = Leaderboard.getFromScoreSaberToken(this.scoreSaberService.getLeaderboard(trackedScore.getLeaderboardId()));
}
assert leaderboard != null; // This should never be null
scores.add(new ScoreResponse(
trackedScore.getScoreId(),
user.getScoresaberAccount(),
leaderboard,
trackedScore.getPp(),
trackedScore.getRank(),
trackedScore.getScore(),
trackedScore.getModifiedScore(),
trackedScore.getWeight(),
trackedScore.getModifiers(),
trackedScore.getMultiplier(),
trackedScore.getMissedNotes(),
trackedScore.getBadCuts(),
trackedScore.getMaxCombo(),
trackedScore.getAccuracy(),
trackedScore.getDifficulty(),
trackedScore.getTimestamp()
));
}
return scores;
}

@ -34,7 +34,7 @@ public class UserService {
* @throws BadRequestException if the user is not found
*/
public User getUser(String steamId) {
if (!this.validateSteamId(steamId)) {
if (!this.isValidSteamId(steamId)) {
throw new BadRequestException("Invalid steam id");
}
@ -49,13 +49,12 @@ public class UserService {
}
/**
* Creates a user in the database
* Saves a user to the database
*
* @param user the user to create
* @return the created user
* @param user the user to save
*/
public User createUser(User user) {
return this.userRepository.save(user);
public void saveUser(User user) {
this.userRepository.save(user);
}
/**
@ -63,8 +62,11 @@ public class UserService {
*
* @return all users
*/
public List<User> getUsers() {
return (List<User>) this.userRepository.findAll();
public List<User> getUsers(boolean steamIdsOnly) {
if (steamIdsOnly) {
return this.userRepository.fetchOnlySteamIds();
}
return this.userRepository.findAll();
}
/**
@ -73,7 +75,7 @@ public class UserService {
* @param steamId the steam id to validate
* @return if the steam id is valid
*/
public boolean validateSteamId(String steamId) {
public boolean isValidSteamId(String steamId) {
return steamId != null && steamId.length() == 17;
}
}

@ -1,14 +1,14 @@
package cc.fascinated.websocket.impl;
import cc.fascinated.common.ScoreSaberUtils;
import cc.fascinated.model.token.ScoreSaberLeaderboardToken;
import cc.fascinated.model.token.ScoreSaberPlayerScoreToken;
import cc.fascinated.model.token.ScoreSaberScoreToken;
import cc.fascinated.model.token.ScoreSaberWebsocketDataToken;
import cc.fascinated.common.TimeUtils;
import cc.fascinated.model.token.*;
import cc.fascinated.model.user.ScoreSaberAccount;
import cc.fascinated.model.user.User;
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.ScoreSaberService;
import cc.fascinated.services.UserService;
import cc.fascinated.websocket.Websocket;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -20,12 +20,20 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* @author Fascinated (fascinated7)
*/
@Component
@Log4j2(topic = "ScoreSaber Websocket")
public class ScoreSaberWebsocket extends Websocket {
/**
* The interval to force update the user's account.
*/
private static long FORCE_UPDATE_INTERVAL = TimeUnit.HOURS.toMillis(12);
/**
* The Jackson deserializer to use.
*/
@ -46,17 +54,24 @@ public class ScoreSaberWebsocket extends Websocket {
*/
private final PlatformService platformService;
/**
* The ScoreSaber service to use
*/
private final ScoreSaberService scoreSaberService;
@Autowired
public ScoreSaberWebsocket(@NonNull ObjectMapper objectMapper, @NonNull UserService userService, @NonNull QuestDBService questDBService,
@NonNull PlatformService platformService) {
@NonNull PlatformService platformService, @NonNull ScoreSaberService scoreSaberService) {
super("ScoreSaber", "wss://scoresaber.com/ws");
this.objectMapper = objectMapper;
this.userService = userService;
this.questDBService = questDBService;
this.platformService = platformService;
this.scoreSaberService = scoreSaberService;
}
@Override @SneakyThrows
@Override
@SneakyThrows
public void handleMessage(@NonNull TextMessage message) {
String payload = message.getPayload();
if (payload.equals("Connected to the ScoreSaber WSS")) { // Ignore the connection message
@ -74,10 +89,24 @@ public class ScoreSaberWebsocket extends Websocket {
ScoreSaberLeaderboardToken leaderboard = scoreToken.getLeaderboard();
ScoreSaberScoreToken.LeaderboardPlayerInfo player = score.getLeaderboardPlayerInfo();
if (!userService.validateSteamId(player.getId())) { // Validate the Steam ID
// Ensure the player is valid
if (!this.userService.isValidSteamId(player.getId())) {
return;
}
User user = userService.getUser(player.getId());
ScoreSaberAccount scoresaberAccount = user.getScoresaberAccount();
// Ensure the users account is up-to-date
if (scoresaberAccount == null || scoresaberAccount.getLastUpdated().before(new Date(System.currentTimeMillis() - FORCE_UPDATE_INTERVAL))) {
log.info("Updating account for '{}', last update: {}",
player.getName(),
scoresaberAccount == null ? "now" : TimeUtils.format(System.currentTimeMillis() - scoresaberAccount.getLastUpdated().getTime())
);
ScoreSaberAccountToken accountToken = scoreSaberService.getAccount(user);
user.setScoresaberAccount(ScoreSaberAccount.getFromToken(accountToken));
userService.saveUser(user); // Save the user
}
double accuracy = ((double) score.getBaseScore() / leaderboard.getMaxScore()) * 100;
String difficulty = ScoreSaberUtils.parseDifficulty(leaderboard.getDifficulty().getDifficulty());
double pp = platformService.getScoreSaberPlatform().getPp(leaderboard.getStars(), accuracy); // Recalculate the PP