cache scoresaber leaderboards
This commit is contained in:
parent
a8eb2372cb
commit
fe888d9fb6
@ -3,6 +3,7 @@ import { AppStatistics } from "@ssr/common/types/backend/app-statistics";
|
|||||||
import { ScoreSaberScoreModel } from "@ssr/common/model/score/impl/scoresaber-score";
|
import { ScoreSaberScoreModel } from "@ssr/common/model/score/impl/scoresaber-score";
|
||||||
import { AdditionalScoreDataModel } from "@ssr/common/model/additional-score-data/additional-score-data";
|
import { AdditionalScoreDataModel } from "@ssr/common/model/additional-score-data/additional-score-data";
|
||||||
import { BeatSaverMapModel } from "@ssr/common/model/beatsaver/map";
|
import { BeatSaverMapModel } from "@ssr/common/model/beatsaver/map";
|
||||||
|
import { ScoreSaberLeaderboardModel } from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard";
|
||||||
|
|
||||||
export class AppService {
|
export class AppService {
|
||||||
/**
|
/**
|
||||||
@ -13,12 +14,14 @@ export class AppService {
|
|||||||
const trackedScores = await ScoreSaberScoreModel.countDocuments();
|
const trackedScores = await ScoreSaberScoreModel.countDocuments();
|
||||||
const additionalScoresData = await AdditionalScoreDataModel.countDocuments();
|
const additionalScoresData = await AdditionalScoreDataModel.countDocuments();
|
||||||
const cachedBeatSaverMaps = await BeatSaverMapModel.countDocuments();
|
const cachedBeatSaverMaps = await BeatSaverMapModel.countDocuments();
|
||||||
|
const cachedScoreSaberLeaderboards = await ScoreSaberLeaderboardModel.countDocuments();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
trackedPlayers,
|
trackedPlayers,
|
||||||
trackedScores,
|
trackedScores,
|
||||||
additionalScoresData,
|
additionalScoresData,
|
||||||
cachedBeatSaverMaps,
|
cachedBeatSaverMaps,
|
||||||
|
cachedScoreSaberLeaderboards,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
import { Leaderboards } from "@ssr/common/leaderboard";
|
import { Leaderboards } from "@ssr/common/leaderboard";
|
||||||
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
||||||
import { SSRCache } from "@ssr/common/cache";
|
|
||||||
import { LeaderboardResponse } from "@ssr/common/response/leaderboard-response";
|
import { LeaderboardResponse } from "@ssr/common/response/leaderboard-response";
|
||||||
import Leaderboard from "@ssr/common/leaderboard/leaderboard";
|
|
||||||
import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token";
|
import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token";
|
||||||
import { NotFoundError } from "elysia";
|
import { NotFoundError } from "elysia";
|
||||||
import BeatSaverService from "./beatsaver.service";
|
import BeatSaverService from "./beatsaver.service";
|
||||||
import { BeatSaverMap } from "@ssr/common/model/beatsaver/map";
|
import { BeatSaverMap } from "@ssr/common/model/beatsaver/map";
|
||||||
import { getScoreSaberLeaderboardFromToken } from "@ssr/common/token-creators";
|
import { getScoreSaberLeaderboardFromToken } from "@ssr/common/token-creators";
|
||||||
|
import ScoreSaberLeaderboard, {
|
||||||
const leaderboardCache = new SSRCache({
|
ScoreSaberLeaderboardModel,
|
||||||
ttl: 1000 * 60 * 60 * 24,
|
} from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard";
|
||||||
});
|
import Leaderboard from "@ssr/common/model/leaderboard/leaderboard";
|
||||||
|
|
||||||
export default class LeaderboardService {
|
export default class LeaderboardService {
|
||||||
/**
|
/**
|
||||||
@ -21,16 +19,9 @@ export default class LeaderboardService {
|
|||||||
* @param id the id
|
* @param id the id
|
||||||
*/
|
*/
|
||||||
private static async getLeaderboardToken<T>(leaderboard: Leaderboards, id: string): Promise<T | undefined> {
|
private static async getLeaderboardToken<T>(leaderboard: Leaderboards, id: string): Promise<T | undefined> {
|
||||||
const cacheKey = `${leaderboard}-${id}`;
|
|
||||||
if (leaderboardCache.has(cacheKey)) {
|
|
||||||
return leaderboardCache.get(cacheKey) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (leaderboard) {
|
switch (leaderboard) {
|
||||||
case "scoresaber": {
|
case "scoresaber": {
|
||||||
const leaderboard = (await scoresaberService.lookupLeaderboard(id)) as T;
|
return (await scoresaberService.lookupLeaderboard(id)) as T;
|
||||||
leaderboardCache.set(cacheKey, leaderboard);
|
|
||||||
return leaderboard;
|
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -51,14 +42,37 @@ export default class LeaderboardService {
|
|||||||
|
|
||||||
switch (leaderboardName) {
|
switch (leaderboardName) {
|
||||||
case "scoresaber": {
|
case "scoresaber": {
|
||||||
const leaderboardToken = await LeaderboardService.getLeaderboardToken<ScoreSaberLeaderboardToken>(
|
let foundLeaderboard = false;
|
||||||
leaderboardName,
|
const cachedLeaderboard = await ScoreSaberLeaderboardModel.findById(id);
|
||||||
id
|
if (cachedLeaderboard != null) {
|
||||||
);
|
leaderboard = cachedLeaderboard as unknown as ScoreSaberLeaderboard;
|
||||||
if (leaderboardToken == undefined) {
|
if (!leaderboard.shouldRefresh()) {
|
||||||
|
foundLeaderboard = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundLeaderboard) {
|
||||||
|
const leaderboardToken = await LeaderboardService.getLeaderboardToken<ScoreSaberLeaderboardToken>(
|
||||||
|
leaderboardName,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
if (leaderboardToken == undefined) {
|
||||||
|
throw new NotFoundError(`Leaderboard not found for "${id}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
leaderboard = getScoreSaberLeaderboardFromToken(leaderboardToken);
|
||||||
|
leaderboard.lastRefreshed = new Date();
|
||||||
|
|
||||||
|
await ScoreSaberLeaderboardModel.findOneAndUpdate({ _id: id }, leaderboard, {
|
||||||
|
upsert: true,
|
||||||
|
new: true,
|
||||||
|
setDefaultsOnInsert: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (leaderboard == undefined) {
|
||||||
throw new NotFoundError(`Leaderboard not found for "${id}"`);
|
throw new NotFoundError(`Leaderboard not found for "${id}"`);
|
||||||
}
|
}
|
||||||
leaderboard = getScoreSaberLeaderboardFromToken(leaderboardToken);
|
|
||||||
beatSaverMap = await BeatSaverService.getMap(leaderboard.songHash);
|
beatSaverMap = await BeatSaverService.getMap(leaderboard.songHash);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
import Leaderboard from "../leaderboard";
|
|
||||||
import { LeaderboardStatus } from "../leaderboard-status";
|
|
||||||
|
|
||||||
export default interface ScoreSaberLeaderboard extends Leaderboard {
|
|
||||||
/**
|
|
||||||
* The star count for the leaderboard.
|
|
||||||
*/
|
|
||||||
readonly stars: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The total amount of plays.
|
|
||||||
*/
|
|
||||||
readonly plays: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The amount of plays today.
|
|
||||||
*/
|
|
||||||
readonly dailyPlays: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this leaderboard is qualified to be ranked.
|
|
||||||
*/
|
|
||||||
readonly qualified: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The status of the map.
|
|
||||||
*/
|
|
||||||
readonly status: LeaderboardStatus;
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
import { MapDifficulty } from "../score/map-difficulty";
|
|
||||||
import { MapCharacteristic } from "../types/map-characteristic";
|
|
||||||
|
|
||||||
export default interface LeaderboardDifficulty {
|
|
||||||
/**
|
|
||||||
* The id of the leaderboard.
|
|
||||||
*/
|
|
||||||
leaderboardId: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The difficulty of the leaderboard.
|
|
||||||
*/
|
|
||||||
difficulty: MapDifficulty;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The characteristic of the leaderboard.
|
|
||||||
*/
|
|
||||||
characteristic: MapCharacteristic;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The raw difficulty of the leaderboard.
|
|
||||||
*/
|
|
||||||
difficultyRaw: string;
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
import LeaderboardDifficulty from "./leaderboard-difficulty";
|
|
||||||
|
|
||||||
export default interface Leaderboard {
|
|
||||||
/**
|
|
||||||
* The id of the leaderboard.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
readonly id: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The hash of the song this leaderboard is for.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
readonly songHash: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of the song this leaderboard is for.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
readonly songName: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The sub name of the leaderboard.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
readonly songSubName: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The author of the song this leaderboard is for.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
readonly songAuthorName: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The author of the level this leaderboard is for.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
readonly levelAuthorName: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The difficulty of the leaderboard.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
readonly difficulty: LeaderboardDifficulty;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The difficulties of the leaderboard.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
readonly difficulties: LeaderboardDifficulty[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The maximum score of the leaderboard.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
readonly maxScore: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the leaderboard is ranked.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
readonly ranked: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The link to the song art.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
readonly songArt: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The date the leaderboard was created.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
readonly timestamp: Date;
|
|
||||||
}
|
|
@ -0,0 +1,55 @@
|
|||||||
|
import Leaderboard from "../leaderboard";
|
||||||
|
import { type LeaderboardStatus } from "../leaderboard-status";
|
||||||
|
import { getModelForClass, modelOptions, Prop, ReturnModelType, Severity } from "@typegoose/typegoose";
|
||||||
|
import { Document } from "mongoose";
|
||||||
|
|
||||||
|
@modelOptions({
|
||||||
|
options: { allowMixed: Severity.ALLOW },
|
||||||
|
schemaOptions: {
|
||||||
|
collection: "scoresaber-leaderboards",
|
||||||
|
toObject: {
|
||||||
|
virtuals: true,
|
||||||
|
transform: function (_, ret) {
|
||||||
|
ret.id = ret._id;
|
||||||
|
delete ret._id;
|
||||||
|
delete ret.__v;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class ScoreSaberLeaderboard extends Leaderboard {
|
||||||
|
/**
|
||||||
|
* The star count for the leaderboard.
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
readonly stars!: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total amount of plays.
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
readonly plays!: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of plays today.
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
readonly dailyPlays!: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this leaderboard is qualified to be ranked.
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
readonly qualified!: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The status of the map.
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
readonly status!: LeaderboardStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScoreSaberLeaderboardDocument = ScoreSaberLeaderboard & Document;
|
||||||
|
export const ScoreSaberLeaderboardModel: ReturnModelType<typeof ScoreSaberLeaderboard> =
|
||||||
|
getModelForClass(ScoreSaberLeaderboard);
|
@ -0,0 +1,29 @@
|
|||||||
|
import { type MapDifficulty } from "../../score/map-difficulty";
|
||||||
|
import { type MapCharacteristic } from "../../types/map-characteristic";
|
||||||
|
import { Prop } from "@typegoose/typegoose";
|
||||||
|
|
||||||
|
export default class LeaderboardDifficulty {
|
||||||
|
/**
|
||||||
|
* The id of the leaderboard.
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
leaderboardId!: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The difficulty of the leaderboard.
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
difficulty!: MapDifficulty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The characteristic of the leaderboard.
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
characteristic!: MapCharacteristic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raw difficulty of the leaderboard.
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
difficultyRaw!: string;
|
||||||
|
}
|
112
projects/common/src/model/leaderboard/leaderboard.ts
Normal file
112
projects/common/src/model/leaderboard/leaderboard.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import LeaderboardDifficulty from "./leaderboard-difficulty";
|
||||||
|
import { Prop } from "@typegoose/typegoose";
|
||||||
|
|
||||||
|
export default class Leaderboard {
|
||||||
|
/**
|
||||||
|
* The id of the leaderboard.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
private readonly _id!: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hash of the song this leaderboard is for.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
readonly songHash!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the song this leaderboard is for.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
readonly songName!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sub name of the leaderboard.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
readonly songSubName!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The author of the song this leaderboard is for.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
readonly songAuthorName!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The author of the level this leaderboard is for.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
readonly levelAuthorName!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The difficulty of the leaderboard.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@Prop({ required: true, _id: false, type: () => LeaderboardDifficulty })
|
||||||
|
readonly difficulty!: LeaderboardDifficulty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The difficulties of the leaderboard.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@Prop({ required: true, _id: false, type: () => [LeaderboardDifficulty] })
|
||||||
|
readonly difficulties!: LeaderboardDifficulty[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum score of the leaderboard.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
readonly maxScore!: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the leaderboard is ranked.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
readonly ranked!: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The link to the song art.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
readonly songArt!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The date the leaderboard was created.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
readonly timestamp!: Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The date the leaderboard was last refreshed.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
lastRefreshed?: Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should the map data be refreshed?
|
||||||
|
*
|
||||||
|
* @returns true if the map data should be refreshed
|
||||||
|
*/
|
||||||
|
public shouldRefresh(): boolean {
|
||||||
|
if (!this.lastRefreshed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const now = new Date();
|
||||||
|
return now.getTime() - this.lastRefreshed.getTime() > 1000 * 60 * 60 * 24; // 1 day
|
||||||
|
}
|
||||||
|
|
||||||
|
get id(): number {
|
||||||
|
return this._id;
|
||||||
|
}
|
||||||
|
}
|
@ -44,7 +44,7 @@ export class ScoreSaberScoreInternal extends Score {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
@Prop({ required: true })
|
@Prop({ required: true })
|
||||||
public readonly pp!: number;
|
public pp!: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The weight of the score, or undefined if not ranked.
|
* The weight of the score, or undefined if not ranked.
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import ScoreSaberLeaderboard from "./leaderboard/impl/scoresaber-leaderboard";
|
import ScoreSaberLeaderboard from "./model/leaderboard/impl/scoresaber-leaderboard";
|
||||||
import ScoreSaberLeaderboardToken from "./types/token/scoresaber/score-saber-leaderboard-token";
|
import ScoreSaberLeaderboardToken from "./types/token/scoresaber/score-saber-leaderboard-token";
|
||||||
import LeaderboardDifficulty from "./leaderboard/leaderboard-difficulty";
|
import LeaderboardDifficulty from "./model/leaderboard/leaderboard-difficulty";
|
||||||
import { getDifficultyFromScoreSaberDifficulty } from "./utils/scoresaber-utils";
|
import { getDifficultyFromScoreSaberDifficulty } from "./utils/scoresaber-utils";
|
||||||
import { MapCharacteristic } from "./types/map-characteristic";
|
import { MapCharacteristic } from "./types/map-characteristic";
|
||||||
import { LeaderboardStatus } from "./leaderboard/leaderboard-status";
|
import { LeaderboardStatus } from "./model/leaderboard/leaderboard-status";
|
||||||
import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate, parseDate } from "./utils/time-utils";
|
import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate, parseDate } from "./utils/time-utils";
|
||||||
import ScoreSaberPlayerToken from "./types/token/scoresaber/score-saber-player-token";
|
import ScoreSaberPlayerToken from "./types/token/scoresaber/score-saber-player-token";
|
||||||
import ScoreSaberPlayer, { ScoreSaberBadge, ScoreSaberBio } from "./player/impl/scoresaber-player";
|
import ScoreSaberPlayer, { ScoreSaberBadge, ScoreSaberBio } from "./player/impl/scoresaber-player";
|
||||||
|
@ -18,4 +18,9 @@ export type AppStatistics = {
|
|||||||
* The amount of cached BeatSaver maps.
|
* The amount of cached BeatSaver maps.
|
||||||
*/
|
*/
|
||||||
cachedBeatSaverMaps: number;
|
cachedBeatSaverMaps: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of cached ScoreSaber leaderboards.
|
||||||
|
*/
|
||||||
|
cachedScoreSaberLeaderboards: number;
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,7 @@ import LeaderboardScoresResponse from "../response/leaderboard-scores-response";
|
|||||||
import { Page } from "../pagination";
|
import { Page } from "../pagination";
|
||||||
import { ScoreSaberScore } from "src/model/score/impl/scoresaber-score";
|
import { ScoreSaberScore } from "src/model/score/impl/scoresaber-score";
|
||||||
import { PlayerScore } from "../score/player-score";
|
import { PlayerScore } from "../score/player-score";
|
||||||
import ScoreSaberLeaderboard from "../leaderboard/impl/scoresaber-leaderboard";
|
import ScoreSaberLeaderboard from "../model/leaderboard/impl/scoresaber-leaderboard";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the player's scores
|
* Fetches the player's scores
|
||||||
|
@ -28,6 +28,7 @@ export default async function HomePage() {
|
|||||||
<Statistic title="Tracked Scores" value={statistics.trackedScores} />
|
<Statistic title="Tracked Scores" value={statistics.trackedScores} />
|
||||||
<Statistic title="Additional Scores Data" value={statistics.additionalScoresData} />
|
<Statistic title="Additional Scores Data" value={statistics.additionalScoresData} />
|
||||||
<Statistic title="Cached BeatSaver Maps" value={statistics.cachedBeatSaverMaps} />
|
<Statistic title="Cached BeatSaver Maps" value={statistics.cachedBeatSaverMaps} />
|
||||||
|
<Statistic title="Cached ScoreSaber Leaderboards" value={statistics.cachedScoreSaberLeaderboards} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user