store the users scoresaber profile and add more data to the top scores endpoint
This commit is contained in:
parent
0f7a890e44
commit
242a8a2fba
26
API/src/main/java/cc/fascinated/common/DateUtils.java
Normal file
26
API/src/main/java/cc/fascinated/common/DateUtils.java
Normal file
@ -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 lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.text.DecimalFormatSymbols;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fascinated (fascinated7)
|
* @author Fascinated (fascinated7)
|
||||||
*/
|
*/
|
||||||
@ -31,4 +34,19 @@ public class MathUtils {
|
|||||||
public static double lerp(double a, double b, double t) {
|
public static double lerp(double a, double b, double t) {
|
||||||
return a + t * (b - a);
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
166
API/src/main/java/cc/fascinated/common/TimeUtils.java
Normal file
166
API/src/main/java/cc/fascinated/common/TimeUtils.java
Normal file
@ -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;
|
package cc.fascinated.controller;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fascinated (fascinated7)
|
* @author Fascinated (fascinated7)
|
||||||
*/
|
*/
|
||||||
|
@ -38,7 +38,7 @@ public class ScoresController {
|
|||||||
@ResponseBody
|
@ResponseBody
|
||||||
@GetMapping(value = "/top/{platform}")
|
@GetMapping(value = "/top/{platform}")
|
||||||
public ResponseEntity<List<?>> getTopScores(@PathVariable String 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;
|
package cc.fascinated.controller;
|
||||||
|
|
||||||
import cc.fascinated.exception.impl.BadRequestException;
|
import cc.fascinated.exception.impl.BadRequestException;
|
||||||
import cc.fascinated.model.user.User;
|
|
||||||
import cc.fascinated.model.user.UserDTO;
|
import cc.fascinated.model.user.UserDTO;
|
||||||
import cc.fascinated.services.UserService;
|
import cc.fascinated.services.UserService;
|
||||||
import lombok.NonNull;
|
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.Setter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import org.springframework.data.annotation.Id;
|
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;
|
import java.util.UUID;
|
||||||
|
|
||||||
@ -15,6 +17,7 @@ import java.util.UUID;
|
|||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@ToString
|
@ToString
|
||||||
|
@Document("user")
|
||||||
public class User {
|
public class User {
|
||||||
/**
|
/**
|
||||||
* The ID of the user.
|
* The ID of the user.
|
||||||
@ -28,11 +31,13 @@ public class User {
|
|||||||
* Usually their Steam name.
|
* Usually their Steam name.
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
|
@Indexed
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID of the users steam profile.
|
* The ID of the users steam profile.
|
||||||
*/
|
*/
|
||||||
|
@Indexed(unique = true)
|
||||||
private String steamId;
|
private String steamId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,6 +49,11 @@ public class User {
|
|||||||
*/
|
*/
|
||||||
public boolean hasLoggedIn;
|
public boolean hasLoggedIn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's ScoreSaber account token.
|
||||||
|
*/
|
||||||
|
public ScoreSaberAccount scoresaberAccount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the User object to a UserDTO object.
|
* Converts the User object to a UserDTO object.
|
||||||
*
|
*
|
||||||
|
@ -69,20 +69,19 @@ public abstract class Platform {
|
|||||||
public abstract double getPp(double stars, double accuracy);
|
public abstract double getPp(double stars, double accuracy);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called every 10 minutes to update
|
* Called to update the players
|
||||||
* the players data in QuestDB.
|
* data in QuestDB.
|
||||||
*/
|
*/
|
||||||
public abstract void updatePlayers();
|
public abstract void updatePlayers();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called every 10 minutes to update
|
* Called to update the metrics
|
||||||
* the metrics for total scores, etc.
|
* for total scores, etc.
|
||||||
*/
|
*/
|
||||||
public abstract void updateMetrics();
|
public abstract void updateMetrics();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called every day at midnight to update
|
* Called to update the leaderboards.
|
||||||
* the leaderboards.
|
|
||||||
*/
|
*/
|
||||||
public abstract void updateLeaderboards();
|
public abstract void updateLeaderboards();
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ 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.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -153,7 +152,7 @@ public class ScoreSaberPlatform extends Platform {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updatePlayers() {
|
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
|
if (!user.isHasLoggedIn()) { // Check if the user has linked their account
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,9 @@ package cc.fascinated.repository.mongo;
|
|||||||
|
|
||||||
import cc.fascinated.model.user.User;
|
import cc.fascinated.model.user.User;
|
||||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
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.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@ -17,4 +19,21 @@ public interface UserRepository extends MongoRepository<User, UUID> {
|
|||||||
* @return the user
|
* @return the user
|
||||||
*/
|
*/
|
||||||
Optional<User> findBySteamId(String steamId);
|
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.Platform;
|
||||||
import cc.fascinated.platform.impl.ScoreSaberPlatform;
|
import cc.fascinated.platform.impl.ScoreSaberPlatform;
|
||||||
import cc.fascinated.repository.couchdb.TrackedScoreRepository;
|
|
||||||
import com.mongodb.client.model.Filters;
|
import com.mongodb.client.model.Filters;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
@ -59,12 +58,12 @@ public class PlatformService {
|
|||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
@Scheduled(cron = "0 */15 * * * *")
|
@Scheduled(cron = "0 */15 * * * *")
|
||||||
public void updateScores() {
|
public void updatePlayerMetrics() {
|
||||||
log.info("Updating %s platform players...".formatted(this.platforms.size()));
|
log.info("Updating %s platform player metrics...".formatted(this.platforms.size()));
|
||||||
for (Platform platform : this.platforms) {
|
for (Platform platform : this.platforms) {
|
||||||
platform.updatePlayers();
|
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 kong.unirest.core.HttpResponse;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.extern.log4j.Log4j2;
|
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;
|
||||||
|
|
||||||
@ -50,15 +48,15 @@ public class ScoreSaberService {
|
|||||||
*/
|
*/
|
||||||
public ScoreSaberAccountToken getAccount(User user) {
|
public ScoreSaberAccountToken getAccount(User user) {
|
||||||
if (user.getSteamId() == null) {
|
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);
|
HttpResponse<ScoreSaberAccountToken> response = Request.get(GET_PLAYER_ENDPOINT.formatted(user.getSteamId()), ScoreSaberAccountToken.class);
|
||||||
if (response.getParsingError().isPresent()) { // Failed to parse the response
|
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
|
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();
|
return response.getBody();
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
package cc.fascinated.services;
|
package cc.fascinated.services;
|
||||||
|
|
||||||
import cc.fascinated.exception.impl.BadRequestException;
|
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.ScoresOverResponse;
|
||||||
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.user.User;
|
||||||
import cc.fascinated.platform.Platform;
|
import cc.fascinated.platform.Platform;
|
||||||
import cc.fascinated.repository.couchdb.TrackedScoreRepository;
|
import cc.fascinated.repository.couchdb.TrackedScoreRepository;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
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.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,9 +32,24 @@ public class TrackedScoreService {
|
|||||||
@NonNull
|
@NonNull
|
||||||
private final TrackedScoreRepository trackedScoreRepository;
|
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
|
@Autowired
|
||||||
public TrackedScoreService(@NonNull TrackedScoreRepository trackedScoreRepository) {
|
public TrackedScoreService(@NonNull TrackedScoreRepository trackedScoreRepository, @NonNull UserService userService,
|
||||||
|
@NonNull ScoreSaberService scoreSaberService) {
|
||||||
this.trackedScoreRepository = trackedScoreRepository;
|
this.trackedScoreRepository = trackedScoreRepository;
|
||||||
|
this.userService = userService;
|
||||||
|
this.scoreSaberService = scoreSaberService;
|
||||||
this.trackedScoreRepository.ensureDeduplication();
|
this.trackedScoreRepository.ensureDeduplication();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,11 +61,41 @@ public class TrackedScoreService {
|
|||||||
* @param amount the amount of scores to get
|
* @param amount the amount of scores to get
|
||||||
* @return the scores
|
* @return the scores
|
||||||
*/
|
*/
|
||||||
public List<TrackedScore> getTopScores(Platform.Platforms platform, int amount) {
|
public List<ScoreResponse> getTopScores(Platform.Platforms platform, int amount) {
|
||||||
List<TrackedScore> scores = trackedScoreRepository.findTopRankedScores(platform.getPlatformName(), amount);
|
List<TrackedScore> foundScores = trackedScoreRepository.findTopRankedScores(platform.getPlatformName(), amount);
|
||||||
if (scores.isEmpty()) {
|
if (foundScores.isEmpty()) {
|
||||||
throw new BadRequestException("No scores found for platform " + platform.getPlatformName());
|
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;
|
return scores;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ public class UserService {
|
|||||||
* @throws BadRequestException if the user is not found
|
* @throws BadRequestException if the user is not found
|
||||||
*/
|
*/
|
||||||
public User getUser(String steamId) {
|
public User getUser(String steamId) {
|
||||||
if (!this.validateSteamId(steamId)) {
|
if (!this.isValidSteamId(steamId)) {
|
||||||
throw new BadRequestException("Invalid steam id");
|
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
|
* @param user the user to save
|
||||||
* @return the created user
|
|
||||||
*/
|
*/
|
||||||
public User createUser(User user) {
|
public void saveUser(User user) {
|
||||||
return this.userRepository.save(user);
|
this.userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,8 +62,11 @@ public class UserService {
|
|||||||
*
|
*
|
||||||
* @return all users
|
* @return all users
|
||||||
*/
|
*/
|
||||||
public List<User> getUsers() {
|
public List<User> getUsers(boolean steamIdsOnly) {
|
||||||
return (List<User>) this.userRepository.findAll();
|
if (steamIdsOnly) {
|
||||||
|
return this.userRepository.fetchOnlySteamIds();
|
||||||
|
}
|
||||||
|
return this.userRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -73,7 +75,7 @@ public class UserService {
|
|||||||
* @param steamId the steam id to validate
|
* @param steamId the steam id to validate
|
||||||
* @return if the steam id is valid
|
* @return if the steam id is valid
|
||||||
*/
|
*/
|
||||||
public boolean validateSteamId(String steamId) {
|
public boolean isValidSteamId(String steamId) {
|
||||||
return steamId != null && steamId.length() == 17;
|
return steamId != null && steamId.length() == 17;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package cc.fascinated.websocket.impl;
|
package cc.fascinated.websocket.impl;
|
||||||
|
|
||||||
import cc.fascinated.common.ScoreSaberUtils;
|
import cc.fascinated.common.ScoreSaberUtils;
|
||||||
import cc.fascinated.model.token.ScoreSaberLeaderboardToken;
|
import cc.fascinated.common.TimeUtils;
|
||||||
import cc.fascinated.model.token.ScoreSaberPlayerScoreToken;
|
import cc.fascinated.model.token.*;
|
||||||
import cc.fascinated.model.token.ScoreSaberScoreToken;
|
import cc.fascinated.model.user.ScoreSaberAccount;
|
||||||
import cc.fascinated.model.token.ScoreSaberWebsocketDataToken;
|
import cc.fascinated.model.user.User;
|
||||||
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.PlatformService;
|
||||||
import cc.fascinated.services.QuestDBService;
|
import cc.fascinated.services.QuestDBService;
|
||||||
|
import cc.fascinated.services.ScoreSaberService;
|
||||||
import cc.fascinated.services.UserService;
|
import cc.fascinated.services.UserService;
|
||||||
import cc.fascinated.websocket.Websocket;
|
import cc.fascinated.websocket.Websocket;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
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.stereotype.Component;
|
||||||
import org.springframework.web.socket.TextMessage;
|
import org.springframework.web.socket.TextMessage;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fascinated (fascinated7)
|
* @author Fascinated (fascinated7)
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
@Log4j2(topic = "ScoreSaber Websocket")
|
@Log4j2(topic = "ScoreSaber Websocket")
|
||||||
public class ScoreSaberWebsocket extends 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.
|
* The Jackson deserializer to use.
|
||||||
*/
|
*/
|
||||||
@ -46,17 +54,24 @@ public class ScoreSaberWebsocket extends Websocket {
|
|||||||
*/
|
*/
|
||||||
private final PlatformService platformService;
|
private final PlatformService platformService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ScoreSaber service to use
|
||||||
|
*/
|
||||||
|
private final ScoreSaberService scoreSaberService;
|
||||||
|
|
||||||
@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) {
|
@NonNull PlatformService platformService, @NonNull ScoreSaberService scoreSaberService) {
|
||||||
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;
|
this.platformService = platformService;
|
||||||
|
this.scoreSaberService = scoreSaberService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override @SneakyThrows
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
public void handleMessage(@NonNull TextMessage message) {
|
public void handleMessage(@NonNull TextMessage message) {
|
||||||
String payload = message.getPayload();
|
String payload = message.getPayload();
|
||||||
if (payload.equals("Connected to the ScoreSaber WSS")) { // Ignore the connection message
|
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();
|
ScoreSaberLeaderboardToken leaderboard = scoreToken.getLeaderboard();
|
||||||
ScoreSaberScoreToken.LeaderboardPlayerInfo player = score.getLeaderboardPlayerInfo();
|
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;
|
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;
|
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
|
double pp = platformService.getScoreSaberPlatform().getPp(leaderboard.getStars(), accuracy); // Recalculate the PP
|
||||||
|
Reference in New Issue
Block a user