many many many many changes

This commit is contained in:
Lee
2023-10-19 14:17:55 +01:00
parent 6acf6e8635
commit a031451fa3
36 changed files with 2743 additions and 174 deletions

View File

@ -0,0 +1,59 @@
import { fetchBuilder, MemoryCache } from "node-fetch-cache";
export class FetchQueue {
private _fetch;
private _queue: string[];
private _rateLimitReset: number;
constructor(ttl: number) {
this._fetch = fetchBuilder.withCache(
new MemoryCache({
ttl: ttl,
}),
);
this._queue = [];
this._rateLimitReset = Date.now();
}
/**
* Fetches the given url, and handles rate limiting
* re-requesting if the rate limit is exceeded.
*
* @param url the url to fetch
* @returns the response
*/
public async fetch(url: string): Promise<any> {
const now = Date.now();
if (now < this._rateLimitReset) {
this._queue.push(url);
await new Promise<void>((resolve) =>
setTimeout(resolve, this._rateLimitReset - now),
);
}
const response = await this._fetch(url);
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after")) * 1000;
this._queue.push(url);
await new Promise<void>((resolve) => setTimeout(resolve, retryAfter));
return this.fetch(this._queue.shift() as string);
}
if (response.headers.has("x-ratelimit-remaining")) {
const remaining = Number(response.headers.get("x-ratelimit-remaining"));
if (remaining === 0) {
const reset = Number(response.headers.get("x-ratelimit-reset")) * 1000;
this._queue.push(url);
await new Promise<void>((resolve) => setTimeout(resolve, reset - now));
return this.fetch(this._queue.shift() as string);
}
}
if (this._queue.length > 0) {
const nextUrl = this._queue.shift();
return this.fetch(nextUrl as string);
}
return response;
}
}

9
src/utils/numberUtils.ts Normal file
View File

@ -0,0 +1,9 @@
/**
* Checks if the given value is an number.
*
* @param value the number
* @returns true if value is a number, otherwise false
*/
export function isNumber(value: any): boolean {
return !isNaN(value);
}

View File

@ -0,0 +1,90 @@
import { logger } from "@/logger";
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
import { fetchBuilder, MemoryCache } from "node-fetch-cache";
import { formatString } from "../string";
// Create a fetch instance with a cache
const fetch = fetchBuilder.withCache(
new MemoryCache({
ttl: 15 * 60 * 1000, // 15 minutes
}),
);
// Api endpoints
const API_URL = "https://scoresaber.com/api";
const SEARCH_PLAYER_URL =
API_URL + "/players?search={}&page=1&withMetadata=false";
const PLAYER_SCORES =
API_URL + "/player/{}/scores?limit={}&sort={}&page={}&withMetadata=true";
const SearchType = {
RECENT: "recent",
TOP: "top",
};
/**
* Search for a list of players by name
*
* @param name the name to search
* @returns a list of players
*/
export async function searchByName(
name: string,
): Promise<ScoresaberPlayer[] | undefined> {
const response = await fetch(formatString(SEARCH_PLAYER_URL, name));
const json = await response.json();
// Check if there was an error fetching the user data
if (json.errorMessage) {
return undefined;
}
return json.players as ScoresaberPlayer[];
}
export async function fetchScores(
playerId: string,
page: number = 1,
searchType: string = SearchType.RECENT,
limit: number = 100,
): Promise<ScoresaberPlayerScore[] | undefined> {
if (limit > 100) {
logger.warn(
"Scoresaber API only allows a limit of 100 scores per request, limiting to 100.",
);
limit = 100;
}
const response = await fetch(
formatString(PLAYER_SCORES, playerId, limit, searchType, page),
);
const json = await response.json();
// Check if there was an error fetching the user data
if (json.errorMessage) {
return undefined;
}
return json.playerScores as ScoresaberPlayerScore[];
}
export async function fetchAllScores(
playerId: string,
searchType: string,
): Promise<ScoresaberPlayerScore[] | undefined> {
const scores = new Array();
let done = false,
page = 1;
do {
const response = await fetchScores(playerId, page, searchType);
if (response == undefined || response.length === 0) {
done = true;
break;
}
scores.push(...response);
page++;
} while (!done);
return scores as ScoresaberPlayerScore[];
}

View File

@ -0,0 +1,92 @@
import { ScoreSaberLeaderboard } from "@/database/schemas/scoresaberLeaderboard";
import { ScoresaberScore } from "@/database/schemas/scoresaberScore";
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
export async function createScore(
playerId: string,
scoreSaberScore: ScoresaberPlayerScore,
) {
const score = scoreSaberScore.score;
const leaderboard = scoreSaberScore.leaderboard;
await ScoresaberScore.create({
_id: score.id,
playerId: playerId,
leaderboardId: leaderboard.id,
rank: score.rank,
baseScore: score.baseScore,
modifiedScore: score.modifiedScore,
pp: score.pp,
weight: score.weight,
modifiers: score.modifiers,
multiplier: score.multiplier,
badCuts: score.badCuts,
missedNotes: score.missedNotes,
maxCombo: score.maxCombo,
fullCombo: score.fullCombo,
hmd: score.hmd,
hasReply: score.hasReply,
timeSet: new Date(score.timeSet).getTime(),
});
await ScoreSaberLeaderboard.updateOne(
{ _id: leaderboard.id },
{
_id: leaderboard.id,
songHash: leaderboard.songHash,
songName: leaderboard.songName,
songSubName: leaderboard.songSubName,
songAuthorName: leaderboard.songAuthorName,
levelAuthorName: leaderboard.levelAuthorName,
difficulty: leaderboard.difficulty,
maxScore: leaderboard.maxScore,
createdDate: leaderboard.createdDate,
rankedDate: leaderboard.rankedDate,
qualifiedDate: leaderboard.qualifiedDate,
lovedDate: leaderboard.lovedDate,
ranked: leaderboard.ranked,
qualified: leaderboard.qualified,
loved: leaderboard.loved,
maxPP: leaderboard.maxPP,
stars: leaderboard.stars,
positiveModifiers: leaderboard.positiveModifiers,
plays: leaderboard.plays,
dailyPlays: leaderboard.dailyPlays,
coverImage: leaderboard.coverImage,
difficulties: leaderboard.difficulties,
},
{ upsert: true },
);
}
export async function updateScore(
playerId: string,
scoreSaberScore: ScoresaberPlayerScore,
) {
const score = scoreSaberScore.score;
const leaderboard = scoreSaberScore.leaderboard;
// Delete the old score
await ScoresaberScore.deleteOne({ _id: score.id });
// Create the new score
await ScoresaberScore.create({
_id: score.id,
playerId: playerId,
leaderboardId: leaderboard.id,
rank: score.rank,
baseScore: score.baseScore,
modifiedScore: score.modifiedScore,
pp: score.pp,
weight: score.weight,
modifiers: score.modifiers,
multiplier: score.multiplier,
badCuts: score.badCuts,
missedNotes: score.missedNotes,
maxCombo: score.maxCombo,
fullCombo: score.fullCombo,
hmd: score.hmd,
hasReply: score.hasReply,
timeSet: score.timeSet,
});
}

18
src/utils/string.ts Normal file
View File

@ -0,0 +1,18 @@
/**
* Formats a string with the given arguments.
*
* @param str the string to check
* @param args the arguments to replace
* @returns the formatted string
*/
export function formatString(str: string, ...args: any[]): string {
return str.replace(/{}/g, (match) => {
// If there are no arguments, return the match
if (args.length === 0) {
return match;
}
// Otherwise, return the next argument
return String(args.shift());
});
}