add beatleader score fetching and add icons to FC
Some checks failed
deploy / deploy (push) Failing after 2s

This commit is contained in:
Lee
2023-10-22 13:47:56 +01:00
parent 2e93a1b27f
commit 80e6c0da43
33 changed files with 979 additions and 124 deletions

View File

@ -0,0 +1,347 @@
"use client";
import { BeatleaderSmallerScore } from "@/schemas/beatleader/smaller/smallerScore";
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
import { BeatLeaderAPI } from "@/utils/beatleader/api";
import moment from "moment";
import { toast } from "react-toastify";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { useSettingsStore } from "./settingsStore";
type Player = {
id: string;
scores: BeatleaderSmallerScore[];
};
interface BeatLeaderScoresStore {
lastUpdated: number;
players: Player[];
/**
* Sets when the player scores were last updated
*
* @param lastUpdated when the player scores were last updated
*/
setLastUpdated: (lastUpdated: number) => void;
/**
* Checks if the player exists
*
* @param playerId the player id
* @returns if the player exists
*/
exists: (playerId: string) => boolean;
/**
* Gets the given player
*
* @param playerId the player id
* @returns the player
*/
get(playerId: string): Player | undefined;
/**
* Gets the score for the given player and song hash
*
* @param playerId the player id
* @param songHash the song hash
*/
getScore(
playerId: string,
songHash: string,
): BeatleaderSmallerScore | undefined;
/**
* Adds the player to the local database
*
* @param playerId the player id
* @param callback a callback to call when a score page is fetched
* @returns if the player was added successfully
*/
addPlayer: (
playerId: string,
callback?: (page: number, totalPages: number) => void,
) => Promise<{
error: boolean;
message: string;
}>;
/**
* Refreshes the player scores and adds any new scores to the local database
*/
updatePlayerScores: () => void;
}
const UPDATE_INTERVAL = 1000 * 60 * 30; // 30 minutes
export const useBeatLeaderScoresStore = create<BeatLeaderScoresStore>()(
persist(
(set) => ({
lastUpdated: 0,
players: [],
setLastUpdated: (lastUpdated: number) => {
set({ lastUpdated });
},
exists: (playerId: string) => {
const players: Player[] = useBeatLeaderScoresStore.getState().players;
return players.some((player) => player.id == playerId);
},
get: (playerId: string) => {
const players: Player[] = useBeatLeaderScoresStore.getState().players;
return players.find((player) => player.id == playerId);
},
getScore: (playerId: string, songHash: string) => {
const player = useBeatLeaderScoresStore.getState().get(playerId);
if (player == undefined) return undefined;
return player.scores.find(
(score) => score.leaderboard.song.hash == songHash,
);
},
addPlayer: async (
playerId: string,
callback?: (
page: number,
totalPages: number,
leaderboardName: string,
) => void,
) => {
const players = useBeatLeaderScoresStore.getState().players;
// Check if the player already exists
if (useBeatLeaderScoresStore.getState().exists(playerId)) {
return {
error: true,
message: "Player already exists",
};
}
// Get all of the players scores
const scores = await BeatLeaderAPI.fetchAllScores(
playerId,
(page, totalPages) => {
if (callback) callback(page, totalPages, "BeatLeader");
},
);
if (scores == undefined) {
return {
error: true,
message: "Could not fetch beatleader scores for player",
};
}
let smallerScores = new Array<BeatleaderSmallerScore>();
for (const score of scores) {
// We have to do this to limit the amount of data we store
// so we don't exceed the local storage limit
smallerScores.push({
id: score.id,
accLeft: score.accLeft,
accRight: score.accRight,
fcAccuracy: score.fcAccuracy,
wallsHit: score.wallsHit,
replay: score.replay,
leaderboard: {
song: {
bpm: score.leaderboard.song.bpm,
hash: score.leaderboard.song.hash,
},
},
scoreImprovement:
score.scoreImprovement != null
? {
score: score.scoreImprovement.score,
accuracy: score.scoreImprovement.accuracy,
accRight: score.scoreImprovement.accRight,
accLeft: score.scoreImprovement.accLeft,
badCuts: score.scoreImprovement.badCuts,
missedNotes: score.scoreImprovement.missedNotes,
bombCuts: score.scoreImprovement.bombCuts,
}
: null,
timepost: score.timepost,
});
}
// Remove scores that are already in the database
const player = useBeatLeaderScoresStore.getState().get(playerId);
if (player) {
smallerScores = smallerScores.filter(
(score) => player.scores.findIndex((s) => s.id == score.id) == -1,
);
}
set({
lastUpdated: Date.now(),
players: [
...players,
{
id: playerId,
scores: smallerScores,
},
],
});
return {
error: false,
message: "Player added successfully",
};
},
updatePlayerScores: async () => {
const players = useBeatLeaderScoresStore.getState().players;
const friends = useSettingsStore.getState().friends;
let allPlayers = new Array<ScoresaberPlayer>();
for (const friend of friends) {
allPlayers.push(friend);
}
const localPlayer = useSettingsStore.getState().player;
if (localPlayer) {
allPlayers.push(localPlayer);
}
// add local player and friends if they don't exist
for (const player of allPlayers) {
if (useBeatLeaderScoresStore.getState().get(player.id) == undefined) {
toast.info(
`${
player.id == localPlayer?.id
? `You were`
: `Friend ${player.name} was`
} missing from the BeatLeader scores database, adding...`,
);
await useBeatLeaderScoresStore.getState().addPlayer(player.id);
toast.success(
`${
player.id == useSettingsStore.getState().player?.id
? `You were`
: `Friend ${player.name} was`
} added to the BeatLeader scores database`,
);
}
}
// Skip if we refreshed the scores recently
const timeUntilRefreshMs =
UPDATE_INTERVAL -
(Date.now() - useBeatLeaderScoresStore.getState().lastUpdated);
if (timeUntilRefreshMs > 0) {
console.log(
"Waiting",
moment.duration(timeUntilRefreshMs).humanize(),
"to refresh scores for players",
);
setTimeout(
() => useBeatLeaderScoresStore.getState().updatePlayerScores(),
timeUntilRefreshMs,
);
return;
}
// loop through all of the players and update their scores
for (const player of players) {
if (player == undefined) continue;
console.log(`Updating scores for ${player.id}...`);
let newPlayers = players;
let oldScores = player.scores;
// Sort the scores by date (newset to oldest), so we know when to stop searching for new scores
oldScores = oldScores.sort((a, b) => {
return a.timepost - b.timepost;
});
if (!oldScores.length) return;
const mostRecentScore = oldScores[0];
let search = true;
let page = 0;
let newScoresCount = 0;
while (search) {
page++;
const newScores = await BeatLeaderAPI.fetchScores(player.id, page);
if (newScores == undefined) continue;
for (const score of newScores.scores) {
if (mostRecentScore && score.id == mostRecentScore.id) {
search = false;
break;
}
// remove the old score
const oldScoreIndex = oldScores.findIndex(
(score) => score.id == score.id,
);
if (oldScoreIndex != -1) {
oldScores = oldScores.splice(oldScoreIndex, 1);
}
oldScores.push({
id: score.id,
accLeft: score.accLeft,
accRight: score.accRight,
fcAccuracy: score.fcAccuracy,
wallsHit: score.wallsHit,
replay: score.replay,
leaderboard: {
song: {
bpm: score.leaderboard.song.bpm,
hash: score.leaderboard.song.hash,
},
},
scoreImprovement:
score.scoreImprovement != null
? {
score: score.scoreImprovement.score,
accuracy: score.scoreImprovement.accuracy,
accRight: score.scoreImprovement.accRight,
accLeft: score.scoreImprovement.accLeft,
badCuts: score.scoreImprovement.badCuts,
missedNotes: score.scoreImprovement.missedNotes,
bombCuts: score.scoreImprovement.bombCuts,
}
: null,
timepost: score.timepost,
});
newScoresCount++;
}
}
// Remove the player if it already exists
newPlayers = newPlayers.filter((playerr) => playerr.id != player.id);
// Add the player
newPlayers.push({
id: player.id,
scores: oldScores,
});
if (newScoresCount > 0) {
console.log(
`Found ${newScoresCount} new beatleader scores for ${player.id}`,
);
}
set({
players: newPlayers,
lastUpdated: Date.now(),
});
console.log(friends);
}
},
}),
{
name: "beatleaderScores",
storage: createJSONStorage(() => localStorage),
version: 2,
migrate: (state: any, version: number) => {
state.scores = [];
return state;
},
},
),
);

View File

@ -1,8 +1,8 @@
"use client";
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
import { fetchAllScores, fetchScores } from "@/utils/scoresaber/api";
import { ScoresaberSmallerPlayerScore } from "@/schemas/scoresaber/smaller/smallerPlayerScore";
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
import moment from "moment";
import { toast } from "react-toastify";
import { create } from "zustand";
@ -12,11 +12,11 @@ import { useSettingsStore } from "./settingsStore";
type Player = {
id: string;
scores: {
scoresaber: ScoresaberPlayerScore[];
scoresaber: ScoresaberSmallerPlayerScore[];
};
};
interface PlayerScoresStore {
interface ScoreSaberScoresStore {
lastUpdated: number;
players: Player[];
@ -66,7 +66,7 @@ interface PlayerScoresStore {
const UPDATE_INTERVAL = 1000 * 60 * 30; // 30 minutes
export const usePlayerScoresStore = create<PlayerScoresStore>()(
export const useScoresaberScoresStore = create<ScoreSaberScoresStore>()(
persist(
(set) => ({
lastUpdated: 0,
@ -77,12 +77,12 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
},
exists: (playerId: string) => {
const players: Player[] = usePlayerScoresStore.getState().players;
const players: Player[] = useScoresaberScoresStore.getState().players;
return players.some((player) => player.id == playerId);
},
get: (playerId: string) => {
const players: Player[] = usePlayerScoresStore.getState().players;
const players: Player[] = useScoresaberScoresStore.getState().players;
return players.find((player) => player.id == playerId);
},
@ -90,10 +90,10 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
playerId: string,
callback?: (page: number, totalPages: number) => void,
) => {
const players = usePlayerScoresStore.getState().players;
const players = useScoresaberScoresStore.getState().players;
// Check if the player already exists
if (usePlayerScoresStore.getState().exists(playerId)) {
if (useScoresaberScoresStore.getState().exists(playerId)) {
return {
error: true,
message: "Player already exists",
@ -101,23 +101,53 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
}
// Get all of the players scores
let scores = await fetchAllScores(
let scores = await ScoreSaberAPI.fetchAllScores(
playerId,
"recent",
(page, totalPages) => {
if (callback) callback(page, totalPages);
},
);
if (scores == undefined) {
return {
error: true,
message: "Could not fetch scores for player",
};
}
let smallerScores = new Array<ScoresaberSmallerPlayerScore>();
for (const score of scores) {
smallerScores.push({
score: {
id: score.score.id,
rank: score.score.rank,
baseScore: score.score.baseScore,
modifiedScore: score.score.modifiedScore,
pp: score.score.pp,
weight: score.score.weight,
modifiers: score.score.modifiers,
multiplier: score.score.multiplier,
badCuts: score.score.badCuts,
missedNotes: score.score.missedNotes,
maxCombo: score.score.maxCombo,
fullCombo: score.score.fullCombo,
hmd: score.score.hmd,
timeSet: score.score.timeSet,
},
leaderboard: {
id: score.leaderboard.id,
songHash: score.leaderboard.songHash,
difficulty: score.leaderboard.difficulty,
maxScore: score.leaderboard.maxScore,
createdDate: score.leaderboard.createdDate,
stars: score.leaderboard.stars,
plays: score.leaderboard.plays,
coverImage: score.leaderboard.coverImage,
},
});
}
// Remove scores that are already in the database
const player = usePlayerScoresStore.getState().get(playerId);
const player = useScoresaberScoresStore.getState().get(playerId);
if (player) {
scores = scores.filter(
(score) =>
@ -145,7 +175,7 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
},
updatePlayerScores: async () => {
const players = usePlayerScoresStore.getState().players;
const players = useScoresaberScoresStore.getState().players;
const friends = useSettingsStore.getState().friends;
let allPlayers = new Array<ScoresaberPlayer>();
@ -159,21 +189,21 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
// add local player and friends if they don't exist
for (const player of allPlayers) {
if (usePlayerScoresStore.getState().get(player.id) == undefined) {
if (useScoresaberScoresStore.getState().get(player.id) == undefined) {
toast.info(
`${
player.id == localPlayer?.id
? `You were`
: `Friend ${player.name} was`
} missing from the scores database, adding...`,
} missing from the ScoreSaber scores database, adding...`,
);
await usePlayerScoresStore.getState().addPlayer(player.id);
await useScoresaberScoresStore.getState().addPlayer(player.id);
toast.success(
`${
player.id == useSettingsStore.getState().player?.id
? `You were`
: `Friend ${player.name} was`
} added to the scores database`,
} added to the ScoreSaber scores database`,
);
}
}
@ -181,7 +211,7 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
// Skip if we refreshed the scores recently
const timeUntilRefreshMs =
UPDATE_INTERVAL -
(Date.now() - usePlayerScoresStore.getState().lastUpdated);
(Date.now() - useScoresaberScoresStore.getState().lastUpdated);
if (timeUntilRefreshMs > 0) {
console.log(
"Waiting",
@ -189,12 +219,13 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
"to refresh scores for players",
);
setTimeout(
() => usePlayerScoresStore.getState().updatePlayerScores(),
() => useScoresaberScoresStore.getState().updatePlayerScores(),
timeUntilRefreshMs,
);
return;
}
// loop through all of the players and update their scores
for (const player of players) {
if (player == undefined) continue;
console.log(`Updating scores for ${player.id}...`);
@ -216,23 +247,50 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
let newScoresCount = 0;
while (search) {
page++;
const newScores = await fetchScores(player.id, page);
const newScores = await ScoreSaberAPI.fetchScores(player.id, page);
if (newScores == undefined) continue;
for (const newScore of newScores.scores) {
if (mostRecentScore && newScore.score.id == mostRecentScore.id) {
for (const score of newScores.scores) {
if (mostRecentScore && score.score.id == mostRecentScore.id) {
search = false;
break;
}
// remove the old score
const oldScoreIndex = oldScores.findIndex(
(score) => score.score.id == newScore.score.id,
(score) => score.score.id == score.score.id,
);
if (oldScoreIndex != -1) {
oldScores = oldScores.splice(oldScoreIndex, 1);
}
oldScores.push(newScore);
oldScores.push({
score: {
id: score.score.id,
rank: score.score.rank,
baseScore: score.score.baseScore,
modifiedScore: score.score.modifiedScore,
pp: score.score.pp,
weight: score.score.weight,
modifiers: score.score.modifiers,
multiplier: score.score.multiplier,
badCuts: score.score.badCuts,
missedNotes: score.score.missedNotes,
maxCombo: score.score.maxCombo,
fullCombo: score.score.fullCombo,
hmd: score.score.hmd,
timeSet: score.score.timeSet,
},
leaderboard: {
id: score.leaderboard.id,
songHash: score.leaderboard.songHash,
difficulty: score.leaderboard.difficulty,
maxScore: score.leaderboard.maxScore,
createdDate: score.leaderboard.createdDate,
stars: score.leaderboard.stars,
plays: score.leaderboard.plays,
coverImage: score.leaderboard.coverImage,
},
});
newScoresCount++;
}
}
@ -261,7 +319,7 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
},
}),
{
name: "playerScores",
name: "scoresaberScores",
storage: createJSONStorage(() => localStorage),
version: 1,
@ -282,9 +340,3 @@ export const usePlayerScoresStore = create<PlayerScoresStore>()(
},
),
);
// Update the player scores every 30 minutes
usePlayerScoresStore.getState().updatePlayerScores();
setInterval(() => {
usePlayerScoresStore.getState().updatePlayerScores();
}, UPDATE_INTERVAL);

View File

@ -2,7 +2,7 @@
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
import { SortType, SortTypes } from "@/types/SortTypes";
import { getPlayerInfo } from "@/utils/scoresaber/api";
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
import moment from "moment";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
@ -48,7 +48,7 @@ export const useSettingsStore = create<SettingsStore>()(
return false;
}
const friend = await getPlayerInfo(friendId);
const friend = await ScoreSaberAPI.getPlayerInfo(friendId);
if (friend == undefined || friend == null) return false;
set({ friends: [...friends, friend] });