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 { AdditionalScoreDataModel } from "@ssr/common/model/additional-score-data/additional-score-data";
|
||||
import { BeatSaverMapModel } from "@ssr/common/model/beatsaver/map";
|
||||
import { ScoreSaberLeaderboardModel } from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard";
|
||||
|
||||
export class AppService {
|
||||
/**
|
||||
@ -13,12 +14,14 @@ export class AppService {
|
||||
const trackedScores = await ScoreSaberScoreModel.countDocuments();
|
||||
const additionalScoresData = await AdditionalScoreDataModel.countDocuments();
|
||||
const cachedBeatSaverMaps = await BeatSaverMapModel.countDocuments();
|
||||
const cachedScoreSaberLeaderboards = await ScoreSaberLeaderboardModel.countDocuments();
|
||||
|
||||
return {
|
||||
trackedPlayers,
|
||||
trackedScores,
|
||||
additionalScoresData,
|
||||
cachedBeatSaverMaps,
|
||||
cachedScoreSaberLeaderboards,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,15 @@
|
||||
import { Leaderboards } from "@ssr/common/leaderboard";
|
||||
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
||||
import { SSRCache } from "@ssr/common/cache";
|
||||
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 { NotFoundError } from "elysia";
|
||||
import BeatSaverService from "./beatsaver.service";
|
||||
import { BeatSaverMap } from "@ssr/common/model/beatsaver/map";
|
||||
import { getScoreSaberLeaderboardFromToken } from "@ssr/common/token-creators";
|
||||
|
||||
const leaderboardCache = new SSRCache({
|
||||
ttl: 1000 * 60 * 60 * 24,
|
||||
});
|
||||
import ScoreSaberLeaderboard, {
|
||||
ScoreSaberLeaderboardModel,
|
||||
} from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard";
|
||||
import Leaderboard from "@ssr/common/model/leaderboard/leaderboard";
|
||||
|
||||
export default class LeaderboardService {
|
||||
/**
|
||||
@ -21,16 +19,9 @@ export default class LeaderboardService {
|
||||
* @param id the id
|
||||
*/
|
||||
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) {
|
||||
case "scoresaber": {
|
||||
const leaderboard = (await scoresaberService.lookupLeaderboard(id)) as T;
|
||||
leaderboardCache.set(cacheKey, leaderboard);
|
||||
return leaderboard;
|
||||
return (await scoresaberService.lookupLeaderboard(id)) as T;
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
@ -51,6 +42,16 @@ export default class LeaderboardService {
|
||||
|
||||
switch (leaderboardName) {
|
||||
case "scoresaber": {
|
||||
let foundLeaderboard = false;
|
||||
const cachedLeaderboard = await ScoreSaberLeaderboardModel.findById(id);
|
||||
if (cachedLeaderboard != null) {
|
||||
leaderboard = cachedLeaderboard as unknown as ScoreSaberLeaderboard;
|
||||
if (!leaderboard.shouldRefresh()) {
|
||||
foundLeaderboard = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundLeaderboard) {
|
||||
const leaderboardToken = await LeaderboardService.getLeaderboardToken<ScoreSaberLeaderboardToken>(
|
||||
leaderboardName,
|
||||
id
|
||||
@ -58,7 +59,20 @@ export default class LeaderboardService {
|
||||
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}"`);
|
||||
}
|
||||
|
||||
beatSaverMap = await BeatSaverService.getMap(leaderboard.songHash);
|
||||
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
|
||||
*/
|
||||
@Prop({ required: true })
|
||||
public readonly pp!: number;
|
||||
public pp!: number;
|
||||
|
||||
/**
|
||||
* 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 LeaderboardDifficulty from "./leaderboard/leaderboard-difficulty";
|
||||
import LeaderboardDifficulty from "./model/leaderboard/leaderboard-difficulty";
|
||||
import { getDifficultyFromScoreSaberDifficulty } from "./utils/scoresaber-utils";
|
||||
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 ScoreSaberPlayerToken from "./types/token/scoresaber/score-saber-player-token";
|
||||
import ScoreSaberPlayer, { ScoreSaberBadge, ScoreSaberBio } from "./player/impl/scoresaber-player";
|
||||
|
@ -18,4 +18,9 @@ export type AppStatistics = {
|
||||
* The amount of cached BeatSaver maps.
|
||||
*/
|
||||
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 { ScoreSaberScore } from "src/model/score/impl/scoresaber-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
|
||||
|
@ -28,6 +28,7 @@ export default async function HomePage() {
|
||||
<Statistic title="Tracked Scores" value={statistics.trackedScores} />
|
||||
<Statistic title="Additional Scores Data" value={statistics.additionalScoresData} />
|
||||
<Statistic title="Cached BeatSaver Maps" value={statistics.cachedBeatSaverMaps} />
|
||||
<Statistic title="Cached ScoreSaber Leaderboards" value={statistics.cachedScoreSaberLeaderboards} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
Reference in New Issue
Block a user