From 407bcc866b32696b731c435ff8cc03212e05cccf Mon Sep 17 00:00:00 2001 From: Liam Date: Fri, 24 Nov 2023 20:43:23 +0000 Subject: [PATCH] defer beatsaver map data loading & fix it being fucked --- src/app/api/beatsaver/mapdata/route.ts | 61 +++++++---- src/app/player/[id]/[sort]/[page]/page.tsx | 7 +- src/components/player/Scores.tsx | 113 +++++++++++---------- src/components/player/score/MapButtons.tsx | 94 +++++++++++++++++ src/components/player/score/Score.tsx | 72 ++----------- src/utils/beatsaver/api.ts | 3 +- src/utils/fetchWithQueue.ts | 2 +- src/utils/scoresaber/api.ts | 2 + 8 files changed, 206 insertions(+), 148 deletions(-) create mode 100644 src/components/player/score/MapButtons.tsx diff --git a/src/app/api/beatsaver/mapdata/route.ts b/src/app/api/beatsaver/mapdata/route.ts index 56fbd84..828f6e7 100644 --- a/src/app/api/beatsaver/mapdata/route.ts +++ b/src/app/api/beatsaver/mapdata/route.ts @@ -4,11 +4,24 @@ import { BeatsaverAPI } from "@/utils/beatsaver/api"; export async function GET(request: Request) { const { searchParams } = new URL(request.url); - const mapHashes = searchParams.get("hashes")?.split(",") ?? undefined; + const mapHashes = searchParams.get("hashes"); if (!mapHashes) { return new Response("mapHashes parameter is required", { status: 400 }); } + let toFetch: any[] = []; + if (mapHashes.includes(",")) { + const parts = mapHashes.substring(0, mapHashes.length - 1).split(","); + toFetch.push(...parts); + } else { + toFetch.push(mapHashes); + } + // Convert all hashes to uppercase + for (const hash of toFetch) { + toFetch[toFetch.indexOf(hash)] = hash.toUpperCase(); + } + // Remove duplicates + toFetch = toFetch.filter((hash, index) => toFetch.indexOf(hash) === index); const idOnly = searchParams.get("idonly") === "true"; let totalInCache = 0; @@ -24,39 +37,51 @@ export async function GET(request: Request) { const fetchAndCacheMap = async (mapHash: string) => { const beatSaverMap = await BeatsaverAPI.fetchMapByHash(mapHash); + if (beatSaverMap) { - maps[mapHash] = idOnly ? { id: beatSaverMap.id } : beatSaverMap; - await ( - await Redis.client - ).set( - `beatsaver:map:${mapHash}`, - JSON.stringify(idOnly ? { id: beatSaverMap.id } : beatSaverMap), - "EX", - 60 * 60 * 24 * 7, - ); + addMap(mapHash, beatSaverMap); + await cacheMap(mapHash, beatSaverMap); } }; - for (const mapHash of mapHashes) { + const cacheMap = async (mapHash: string, map: BeatsaverMap) => { + await ( + await Redis.client + ).set( + `beatsaver:map:${mapHash}`, + JSON.stringify(idOnly ? { id: map.id } : map), + "EX", + 60 * 60 * 24 * 7, + ); + }; + + const addMap = (mapHash: string, map: any) => { + maps[mapHash] = idOnly ? { id: map.id } : map; + }; + + for (const mapHash of toFetch) { const map = await fetchMapFromCache(mapHash); - if (map) { - maps[mapHash] = JSON.parse(map); + if (map !== null) { + const json = JSON.parse(map); + addMap(mapHash, json); totalInCache++; } } - if (totalInCache === 0) { - const beatSaverMaps = await BeatsaverAPI.fetchMapsByHash(...mapHashes); + if (totalInCache === 0 && toFetch.length > 1) { + const beatSaverMaps = await BeatsaverAPI.fetchMapsByHash(...toFetch); if (beatSaverMaps) { - for (const mapHash of mapHashes) { + for (const mapHash of toFetch) { const beatSaverMap = beatSaverMaps[mapHash.toLowerCase()]; + if (beatSaverMap) { - await fetchAndCacheMap(mapHash); + await cacheMap(mapHash, beatSaverMap); + addMap(mapHash, beatSaverMap); } } } } else { - for (const mapHash of mapHashes) { + for (const mapHash of toFetch) { if (!maps[mapHash]) { await fetchAndCacheMap(mapHash); } diff --git a/src/app/player/[id]/[sort]/[page]/page.tsx b/src/app/player/[id]/[sort]/[page]/page.tsx index 65b9471..3ee6b44 100644 --- a/src/app/player/[id]/[sort]/[page]/page.tsx +++ b/src/app/player/[id]/[sort]/[page]/page.tsx @@ -69,12 +69,7 @@ export async function generateMetadata({ */ async function getData(id: string, page: number, sort: string) { const playerData = await ScoreSaberAPI.fetchPlayerData(id); - const playerScores = await ScoreSaberAPI.fetchScoresWithBeatsaverData( - id, - page, - sort, - 10, - ); + const playerScores = await ScoreSaberAPI.fetchScores(id, page, sort, 10); return { playerData: playerData, playerScores: playerScores, diff --git a/src/components/player/Scores.tsx b/src/components/player/Scores.tsx index c472463..6906b6b 100644 --- a/src/components/player/Scores.tsx +++ b/src/components/player/Scores.tsx @@ -1,7 +1,7 @@ "use client"; import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; -import { ScoresaberScoreWithBeatsaverData } from "@/schemas/scoresaber/scoreWithBeatsaverData"; +import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore"; import { useSettingsStore } from "@/store/settingsStore"; import { SortType, SortTypes } from "@/types/SortTypes"; import { ScoreSaberAPI } from "@/utils/scoresaber/api"; @@ -16,11 +16,11 @@ type PageInfo = { page: number; totalPages: number; sortType: SortType; - scores: Record; + scores: ScoresaberPlayerScore[] | undefined; }; type ScoresProps = { - initalScores: Record | undefined; + initalScores: ScoresaberPlayerScore[] | undefined; initalPage: number; initalSortType: SortType; initalTotalPages?: number; @@ -45,7 +45,7 @@ export default function Scores({ page: initalPage, totalPages: initalTotalPages || 1, sortType: initalSortType, - scores: initalScores ? initalScores : {}, + scores: initalScores, }); const [changedPage, setChangedPage] = useState(false); @@ -61,35 +61,32 @@ export default function Scores({ return; } - ScoreSaberAPI.fetchScoresWithBeatsaverData( - playerId, - page, - sortType.value, - 10, - ).then((scoresResponse) => { - if (!scoresResponse) { - setError(true); - setErrorMessage("No Scores"); - setScores({ ...scores }); - return; - } - setScores({ - ...scores, - scores: scoresResponse.scores, - totalPages: scoresResponse.pageInfo.totalPages, - page: page, - sortType: sortType, - }); - settingsStore?.setLastUsedSortType(sortType); - window.history.pushState( - {}, - "", - `/player/${playerId}/${sortType.value}/${page}`, - ); - setChangedPage(true); + ScoreSaberAPI.fetchScores(playerId, page, sortType.value, 10).then( + (scoresResponse) => { + if (!scoresResponse) { + setError(true); + setErrorMessage("No Scores"); + setScores({ ...scores }); + return; + } + setScores({ + ...scores, + scores: scoresResponse.scores, + totalPages: scoresResponse.pageInfo.totalPages, + page: page, + sortType: sortType, + }); + settingsStore?.setLastUsedSortType(sortType); + window.history.pushState( + {}, + "", + `/player/${playerId}/${sortType.value}/${page}`, + ); + setChangedPage(true); - console.log(`Switched page to ${page} with sort ${sortType.value}`); - }); + console.log(`Switched page to ${page} with sort ${sortType.value}`); + }, + ); }, [ changedPage, @@ -151,32 +148,36 @@ export default function Scores({
<> -
- {Object.values(scores.scores).map((scoreData, id) => { - const { score, leaderboard, mapId } = scoreData; + {scores.scores ? ( + <> +
+ {scores.scores.map((scoreData, id) => { + const { score, leaderboard } = scoreData; - return ( - + ); + })} +
+ {/* Pagination */} +
+ { + updateScoresPage(scores.sortType, page); + }} /> - ); - })} -
- {/* Pagination */} -
- { - updateScoresPage(scores.sortType, page); - }} - /> -
+
+ + ) : ( +

No Scores!

+ )}
diff --git a/src/components/player/score/MapButtons.tsx b/src/components/player/score/MapButtons.tsx new file mode 100644 index 0000000..ec5c6c0 --- /dev/null +++ b/src/components/player/score/MapButtons.tsx @@ -0,0 +1,94 @@ +"use client"; + +import BeatSaverLogo from "@/components/icons/BeatSaverLogo"; +import YouTubeLogo from "@/components/icons/YouTubeLogo"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/Tooltip"; +import { Button } from "@/components/ui/button"; +import { ScoresaberLeaderboardInfo } from "@/schemas/scoresaber/leaderboard"; +import { songNameToYouTubeLink } from "@/utils/songUtils"; +import Link from "next/link"; +import { useCallback, useEffect, useState } from "react"; +import CopyBsrButton from "./CopyBsrButton"; + +type MapButtonsProps = { + leaderboard: ScoresaberLeaderboardInfo; +}; + +export default function MapButtons({ leaderboard }: MapButtonsProps) { + const [mapId, setMapId] = useState(undefined); + const hash = leaderboard.songHash; + + const getMapId = useCallback(async () => { + const beatSaberMap = await fetch( + `/api/beatsaver/mapdata?hashes=${hash}&idonly=true`, + ); + if (!beatSaberMap) { + return; + } + const json = await beatSaberMap.json(); + setMapId(json.maps[hash].id); + }, [hash]); + + useEffect(() => { + getMapId(); + }, [getMapId]); + + return ( +
+ {mapId && ( + <> +
+ + + + + + + +

Click to open the map page

+
+
+ + +
+
+ + + + + + + +

Click to view the song on YouTube

+
+
+
+ + )} +
+ ); +} diff --git a/src/components/player/score/Score.tsx b/src/components/player/score/Score.tsx index 2af6bfc..9856b1b 100644 --- a/src/components/player/score/Score.tsx +++ b/src/components/player/score/Score.tsx @@ -1,4 +1,3 @@ -import YouTubeLogo from "@/components/icons/YouTubeLogo"; import { ScoresaberLeaderboardInfo } from "@/schemas/scoresaber/leaderboard"; import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; import { ScoresaberScore } from "@/schemas/scoresaber/score"; @@ -7,7 +6,6 @@ import { getPpGainedFromScore } from "@/utils/scoresaber/scores"; import { scoresaberDifficultyNumberToName, songDifficultyToColor, - songNameToYouTubeLink, } from "@/utils/songUtils"; import { formatDate, formatTimeAgo } from "@/utils/timeUtils"; import { @@ -19,28 +17,19 @@ import { import clsx from "clsx"; import Image from "next/image"; import Link from "next/link"; -import BeatSaverLogo from "../../icons/BeatSaverLogo"; +import { Suspense } from "react"; import HeadsetIcon from "../../icons/HeadsetIcon"; import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/Tooltip"; -import { Button } from "../../ui/button"; import ScoreStatLabel from "../ScoreStatLabel"; -import CopyBsrButton from "./CopyBsrButton"; +import MapButtons from "./MapButtons"; type ScoreProps = { score: ScoresaberScore; player: ScoresaberPlayer; leaderboard: ScoresaberLeaderboardInfo; - ownProfile?: ScoresaberPlayer; - mapId?: string; }; -export default function Score({ - score, - player, - leaderboard, - ownProfile, - mapId, -}: ScoreProps) { +export default function Score({ score, player, leaderboard }: ScoreProps) { const isFullCombo = score.missedNotes + score.badCuts === 0; const diffName = scoresaberDifficultyNumberToName( leaderboard.difficulty.difficulty, @@ -124,58 +113,9 @@ export default function Score({ -
- {mapId && ( - <> -
- - - - - - - -

Click to open the map page

-
-
- - -
-
- - - - - - - -

Click to view the song on YouTube

-
-
-
- - )} -
+ }> + +
diff --git a/src/utils/beatsaver/api.ts b/src/utils/beatsaver/api.ts index 124a95e..11011ba 100644 --- a/src/utils/beatsaver/api.ts +++ b/src/utils/beatsaver/api.ts @@ -25,7 +25,7 @@ async function fetchMapsByHash( BS_GET_MAP_BY_HASH_URL, true, hashes.substring(0, hashes.length - 1), - ) + "?idonly=true", + ), ); const json = await response.json(); @@ -47,6 +47,7 @@ async function fetchMapByHash(hash: string): Promise { const response = await BeatsaverFetchQueue.fetch( formatString(BS_GET_MAP_BY_HASH_URL, true, hash), ); + console.log(formatString(BS_GET_MAP_BY_HASH_URL, true, hash)); const json = await response.json(); // Check if there was an error fetching the user data diff --git a/src/utils/fetchWithQueue.ts b/src/utils/fetchWithQueue.ts index 6970957..415fd49 100644 --- a/src/utils/fetchWithQueue.ts +++ b/src/utils/fetchWithQueue.ts @@ -13,7 +13,7 @@ export class FetchQueue { * @param url - The URL to fetch. * @returns The response. */ - public async fetch(url: string, options?: any): Promise { + public async fetch(url: string, options?: any): Promise { const now = Date.now(); if (now < this.rateLimitReset) { diff --git a/src/utils/scoresaber/api.ts b/src/utils/scoresaber/api.ts index d4f24c3..aaa87d7 100644 --- a/src/utils/scoresaber/api.ts +++ b/src/utils/scoresaber/api.ts @@ -178,6 +178,8 @@ async function fetchScoresWithBeatsaverData( for (const score of scores) { url += `${score.leaderboard.songHash},`; } + url = url.substring(0, url.length - 1); + url += "&idonly=true"; const mapResponse = await fetch(url, { next: { revalidate: 60 * 60 * 24 * 7, // 1 week