move logging to debug and make mappers clickable on the score
All checks were successful
Deploy SSR / deploy (push) Successful in 1m13s

This commit is contained in:
Lee 2024-09-12 15:02:03 +01:00
parent 7db722faea
commit 574b1a6532
6 changed files with 76 additions and 20 deletions

@ -16,7 +16,7 @@ export default class DataFetcher {
* @param data the data to log * @param data the data to log
*/ */
public log(data: unknown) { public log(data: unknown) {
console.log(`[${this.name}]: ${data}`); console.debug(`[${this.name}]: ${data}`);
} }
/** /**

@ -1,6 +1,7 @@
import BeatSaverMap from "@/common/database/types/beatsaver-map";
import { db } from "../../database/database"; import { db } from "../../database/database";
import DataFetcher from "../data-fetcher"; import DataFetcher from "../data-fetcher";
import { BeatSaverMap } from "../types/beatsaver/beatsaver-map"; import { BeatSaverMap as BSMap } from "../types/beatsaver/beatsaver-map";
const API_BASE = "https://api.beatsaver.com"; const API_BASE = "https://api.beatsaver.com";
const LOOKUP_MAP_BY_HASH_ENDPOINT = `${API_BASE}/maps/hash/:query`; const LOOKUP_MAP_BY_HASH_ENDPOINT = `${API_BASE}/maps/hash/:query`;
@ -17,17 +18,18 @@ class BeatSaverFetcher extends DataFetcher {
* @param useProxy whether to use the proxy or not * @param useProxy whether to use the proxy or not
* @returns the map that match the query, or undefined if no map were found * @returns the map that match the query, or undefined if no map were found
*/ */
async getMapBsr(query: string, useProxy = true): Promise<string | undefined> { async lookupMap(query: string, useProxy = true): Promise<BeatSaverMap | undefined> {
this.log(`Looking up the bsr for map hash ${query}...`); const before = performance.now();
this.log(`Looking up map "${query}"...`);
const map = await db.beatSaverMaps.get(query); let map = await db.beatSaverMaps.get(query);
// The map is cached // The map is cached
if (map != undefined) { if (map != undefined) {
this.log(`Found cached bsr ${map.bsr} for map hash ${query}`); this.log(`Found cached map "${query}" in ${(performance.now() - before).toFixed(2)}ms`);
return map.bsr; return map;
} }
const response = await this.fetch<BeatSaverMap>(useProxy, LOOKUP_MAP_BY_HASH_ENDPOINT.replace(":query", query)); const response = await this.fetch<BSMap>(useProxy, LOOKUP_MAP_BY_HASH_ENDPOINT.replace(":query", query));
// Map not found // Map not found
if (response == undefined) { if (response == undefined) {
return undefined; return undefined;
@ -42,9 +44,11 @@ class BeatSaverFetcher extends DataFetcher {
await db.beatSaverMaps.add({ await db.beatSaverMaps.add({
hash: query, hash: query,
bsr: bsr, bsr: bsr,
fullData: response,
}); });
this.log(`Looked up bsr ${bsr} for map hash ${query}`); map = await db.beatSaverMaps.get(query);
return bsr; this.log(`Found map "${query}" in ${(performance.now() - before).toFixed(2)}ms`);
return map;
} }
} }

@ -22,6 +22,7 @@ class ScoreSaberFetcher extends DataFetcher {
* @returns the players that match the query, or undefined if no players were found * @returns the players that match the query, or undefined if no players were found
*/ */
async searchPlayers(query: string, useProxy = true): Promise<ScoreSaberPlayerSearch | undefined> { async searchPlayers(query: string, useProxy = true): Promise<ScoreSaberPlayerSearch | undefined> {
const before = performance.now();
this.log(`Searching for players matching "${query}"...`); this.log(`Searching for players matching "${query}"...`);
const results = await this.fetch<ScoreSaberPlayerSearch>( const results = await this.fetch<ScoreSaberPlayerSearch>(
useProxy, useProxy,
@ -34,6 +35,7 @@ class ScoreSaberFetcher extends DataFetcher {
return undefined; return undefined;
} }
results.players.sort((a, b) => a.rank - b.rank); results.players.sort((a, b) => a.rank - b.rank);
this.log(`Found ${results.players.length} players in ${(performance.now() - before).toFixed(2)}ms`);
return results; return results;
} }
@ -45,8 +47,14 @@ class ScoreSaberFetcher extends DataFetcher {
* @returns the player that matches the ID, or undefined * @returns the player that matches the ID, or undefined
*/ */
async lookupPlayer(playerId: string, useProxy = true): Promise<ScoreSaberPlayer | undefined> { async lookupPlayer(playerId: string, useProxy = true): Promise<ScoreSaberPlayer | undefined> {
const before = performance.now();
this.log(`Looking up player "${playerId}"...`); this.log(`Looking up player "${playerId}"...`);
return await this.fetch<ScoreSaberPlayer>(useProxy, LOOKUP_PLAYER_ENDPOINT.replace(":id", playerId)); const response = await this.fetch<ScoreSaberPlayer>(useProxy, LOOKUP_PLAYER_ENDPOINT.replace(":id", playerId));
if (response === undefined) {
return undefined;
}
this.log(`Found player "${playerId}" in ${(performance.now() - before).toFixed(2)}ms`);
return response;
} }
/** /**
@ -64,14 +72,20 @@ class ScoreSaberFetcher extends DataFetcher {
page: number, page: number,
useProxy = true useProxy = true
): Promise<ScoreSaberPlayerScoresPage | undefined> { ): Promise<ScoreSaberPlayerScoresPage | undefined> {
const before = performance.now();
this.log(`Looking up scores for player "${playerId}", sort "${sort}", page "${page}"...`); this.log(`Looking up scores for player "${playerId}", sort "${sort}", page "${page}"...`);
return await this.fetch<ScoreSaberPlayerScoresPage>( const response = await this.fetch<ScoreSaberPlayerScoresPage>(
useProxy, useProxy,
LOOKUP_PLAYER_SCORES_ENDPOINT.replace(":id", playerId) LOOKUP_PLAYER_SCORES_ENDPOINT.replace(":id", playerId)
.replace(":limit", 8 + "") .replace(":limit", 8 + "")
.replace(":sort", sort) .replace(":sort", sort)
.replace(":page", page.toString()) .replace(":page", page.toString())
); );
if (response === undefined) {
return undefined;
}
this.log(`Found scores for player "${playerId}" in ${(performance.now() - before).toFixed(2)}ms`);
return response;
} }
} }

@ -1,3 +1,4 @@
import { BeatSaverMap as BSMap } from "@/common/data-fetcher/types/beatsaver/beatsaver-map";
import { Entity } from "dexie"; import { Entity } from "dexie";
import Database from "../database"; import Database from "../database";
@ -14,4 +15,9 @@ export default class BeatSaverMap extends Entity<Database> {
* The bsr code for the map. * The bsr code for the map.
*/ */
bsr!: string; bsr!: string;
/**
* The full data for the map.
*/
fullData!: BSMap;
} }

@ -0,0 +1,23 @@
import NextLink from "next/link";
type Props = {
/**
* The link to open in a new tab.
*/
href?: string;
/**
* The children to render.
*/
children: React.ReactNode;
};
export default function FallbackLink({ href, children }: Props) {
return href ? (
<NextLink href={href} target="_blank">
{children}
</NextLink>
) : (
<>{children}</>
);
}

@ -3,15 +3,18 @@
import { copyToClipboard } from "@/common/browser-utils"; import { copyToClipboard } from "@/common/browser-utils";
import { beatsaverFetcher } from "@/common/data-fetcher/impl/beatsaver"; import { beatsaverFetcher } from "@/common/data-fetcher/impl/beatsaver";
import ScoreSaberPlayerScore from "@/common/data-fetcher/types/scoresaber/scoresaber-player-score"; import ScoreSaberPlayerScore from "@/common/data-fetcher/types/scoresaber/scoresaber-player-score";
import BeatSaverMap from "@/common/database/types/beatsaver-map";
import { formatNumberWithCommas } from "@/common/number-utils"; import { formatNumberWithCommas } from "@/common/number-utils";
import { getDifficultyFromScoreSaberDifficulty } from "@/common/scoresaber-utils"; import { getDifficultyFromScoreSaberDifficulty } from "@/common/scoresaber-utils";
import { songDifficultyToColor } from "@/common/song-utils"; import { songDifficultyToColor } from "@/common/song-utils";
import { timeAgo } from "@/common/time-utils"; import { timeAgo } from "@/common/time-utils";
import { songNameToYouTubeLink } from "@/common/youtube-utils"; import { songNameToYouTubeLink } from "@/common/youtube-utils";
import FallbackLink from "@/components/fallback-link";
import YouTubeLogo from "@/components/logos/youtube-logo"; import YouTubeLogo from "@/components/logos/youtube-logo";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { GlobeAmericasIcon, StarIcon } from "@heroicons/react/24/solid"; import { GlobeAmericasIcon, StarIcon } from "@heroicons/react/24/solid";
import clsx from "clsx";
import Image from "next/image"; import Image from "next/image";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import BeatSaverLogo from "../../logos/beatsaver-logo"; import BeatSaverLogo from "../../logos/beatsaver-logo";
@ -27,16 +30,18 @@ type Props = {
export default function Score({ playerScore }: Props) { export default function Score({ playerScore }: Props) {
const { score, leaderboard } = playerScore; const { score, leaderboard } = playerScore;
const { toast } = useToast(); const { toast } = useToast();
const [bsr, setBsr] = useState<string | undefined>(); const [beatSaverMap, setBeatSaverMap] = useState<BeatSaverMap | undefined>();
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const bsrFound = await beatsaverFetcher.getMapBsr(leaderboard.songHash); const beatSaverMap = await beatsaverFetcher.lookupMap(leaderboard.songHash);
setBsr(bsrFound); setBeatSaverMap(beatSaverMap);
})(); })();
}, [playerScore, leaderboard.songHash]); }, [playerScore, leaderboard.songHash]);
const diff = getDifficultyFromScoreSaberDifficulty(leaderboard.difficulty.difficulty); const diff = getDifficultyFromScoreSaberDifficulty(leaderboard.difficulty.difficulty);
const mappersProfile =
beatSaverMap != undefined ? `https://beatsaver.com/profile/${beatSaverMap?.fullData.uploader.id}` : undefined;
return ( return (
<div className="grid gap-2 md:gap-0 pb-2 pt-2 first:pt-0 last:pb-0 grid-cols-[20px 1fr_1fr] md:grid-cols-[0.85fr_5fr_1fr_1.2fr]"> <div className="grid gap-2 md:gap-0 pb-2 pt-2 first:pt-0 last:pb-0 grid-cols-[20px 1fr_1fr] md:grid-cols-[0.85fr_5fr_1fr_1.2fr]">
@ -93,21 +98,25 @@ export default function Score({ playerScore }: Props) {
{leaderboard.songName} {leaderboard.songSubName} {leaderboard.songName} {leaderboard.songSubName}
</p> </p>
<p className="text-sm text-gray-400">{leaderboard.songAuthorName}</p> <p className="text-sm text-gray-400">{leaderboard.songAuthorName}</p>
<p className="text-sm">{leaderboard.levelAuthorName}</p> <FallbackLink href={mappersProfile}>
<p className={clsx("text-sm", mappersProfile && "hover:brightness-75 transform-gpu transition-all")}>
{leaderboard.levelAuthorName}
</p>
</FallbackLink>
</div> </div>
</div> </div>
</div> </div>
<div className="hidden md:flex flex-row flex-wrap gap-1 justify-end"> <div className="hidden md:flex flex-row flex-wrap gap-1 justify-end">
{bsr != undefined && ( {beatSaverMap != undefined && (
<> <>
{/* Copy BSR */} {/* Copy BSR */}
<ScoreButton <ScoreButton
onClick={() => { onClick={() => {
toast({ toast({
title: "Copied!", title: "Copied!",
description: `Copied "!bsr ${bsr}" to your clipboard!`, description: `Copied "!bsr ${beatSaverMap}" to your clipboard!`,
}); });
copyToClipboard(`!bsr ${bsr}`); copyToClipboard(`!bsr ${beatSaverMap.bsr}`);
}} }}
tooltip={<p>Click to copy the bsr code</p>} tooltip={<p>Click to copy the bsr code</p>}
> >
@ -117,7 +126,7 @@ export default function Score({ playerScore }: Props) {
{/* Open map in BeatSaver */} {/* Open map in BeatSaver */}
<ScoreButton <ScoreButton
onClick={() => { onClick={() => {
window.open(`https://beatsaver.com/maps/${bsr}`, "_blank"); window.open(`https://beatsaver.com/maps/${beatSaverMap.bsr}`, "_blank");
}} }}
tooltip={<p>Click to open the map</p>} tooltip={<p>Click to open the map</p>}
> >