diff --git a/projects/backend/.env-example b/projects/backend/.env-example new file mode 100644 index 0000000..e00603d --- /dev/null +++ b/projects/backend/.env-example @@ -0,0 +1,2 @@ +MONGO_URI=mongodb://localhost:27017 +API_URL=http://localhost:8080 \ No newline at end of file diff --git a/projects/backend/src/common/config.ts b/projects/backend/src/common/config.ts index 806c026..3d6f845 100644 --- a/projects/backend/src/common/config.ts +++ b/projects/backend/src/common/config.ts @@ -1,3 +1,4 @@ export const Config = { mongoUri: process.env.MONGO_URI, -} \ No newline at end of file + apiUrl: process.env.API_URL || "https://ssr.fascinated.cc/api", +}; diff --git a/projects/backend/src/service/image.service.tsx b/projects/backend/src/service/image.service.tsx index dcf6005..a1e8225 100644 --- a/projects/backend/src/service/image.service.tsx +++ b/projects/backend/src/service/image.service.tsx @@ -6,25 +6,45 @@ import { getDifficultyFromScoreSaberDifficulty } from "@ssr/common/utils/scoresa import { StarIcon } from "../../components/star-icon"; import { GlobeIcon } from "../../components/globe-icon"; import NodeCache from "node-cache"; -import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token"; import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token"; +import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@ssr/common/types/player/impl/scoresaber-player"; +import { Config } from "../common/config"; -const cache = new NodeCache({ - stdTTL: 60 * 60, // 1 hour - checkperiod: 120, -}); +const cache = new NodeCache({ stdTTL: 60 * 60, checkperiod: 120 }); +const imageOptions = { width: 1200, height: 630 }; export class ImageService { + /** + * Fetches data with caching. + * + * @param cacheKey The key used for caching. + * @param fetchFn The function to fetch data if it's not in cache. + */ + private static async fetchWithCache( + cacheKey: string, + fetchFn: () => Promise + ): Promise { + if (cache.has(cacheKey)) { + return cache.get(cacheKey); + } + + const data = await fetchFn(); + if (data) { + cache.set(cacheKey, data); + } + + return data; + } + /** * The base of the OpenGraph image * * @param children the content of the image - * @private */ public static BaseImage({ children }: { children: React.ReactNode }) { return (
(cacheKey); - } else { - player = await scoresaberService.lookupPlayer(id); - if (player != undefined) { - cache.set(cacheKey, player); - } - } - if (player == undefined) { - return undefined; + private static renderDailyChange(change: number, format: (value: number) => string = formatNumberWithCommas) { + if (change === 0) { + return null; } - return new ImageResponse( - ( - - Player's Avatar -
-

{player.name}

-

{formatPp(player.pp)}pp

-
-
- -

#{formatNumberWithCommas(player.rank)}

-
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - Player's Country -

#{formatNumberWithCommas(player.countryRank)}

-
-
-
-
- ), - { - width: 1200, - height: 630, - emoji: "twemoji", - } + return ( +

0 ? "text-green-400" : "text-red-400"}`}> + {change > 0 ? "+" : ""} + {format(change)} +

); } @@ -93,48 +79,121 @@ export class ImageService { * * @param id the player's id */ - public static async generateLeaderboardImage(id: string) { - const cacheKey = `leaderboard-${id}`; - let leaderboard: undefined | ScoreSaberLeaderboardToken; - if (cache.has(cacheKey)) { - leaderboard = cache.get(cacheKey) as ScoreSaberLeaderboardToken; - } else { - leaderboard = await scoresaberService.lookupLeaderboard(id); - if (leaderboard != undefined) { - cache.set(cacheKey, leaderboard); - } + public static async generatePlayerImage(id: string) { + const player = await this.fetchWithCache(`player-${id}`, async () => { + const token = await scoresaberService.lookupPlayer(id); + return token ? await getScoreSaberPlayerFromToken(token, Config.apiUrl) : undefined; + }); + if (!player) { + return undefined; } - if (leaderboard == undefined) { + + const { statisticChange } = player; + const { daily } = statisticChange ?? {}; + const rankChange = daily?.countryRank ?? 0; + const countryRankChange = daily?.rank ?? 0; + const ppChange = daily?.pp ?? 0; + + return new ImageResponse( + ( + + {/* Player Avatar */} + Player's Avatar + + {/* Player Stats */} +
+ {/* Player Name */} +

{player.name}

+ + {/* Player PP */} +
+

{formatPp(player.pp)}pp

+ {this.renderDailyChange(ppChange)} +
+ + {/* Player Stats */} +
+ {/* Player Rank */} +
+ +

#{formatNumberWithCommas(player.rank)}

+ {this.renderDailyChange(rankChange)} +
+ + {/* Player Country Rank */} +
+ Player's Country +

#{formatNumberWithCommas(player.countryRank)}

+ {this.renderDailyChange(countryRankChange)} +
+
+ + {/* Joined Date */} +

+ Joined ScoreSaber in{" "} + {player.joinedDate.toLocaleString("en-US", { + timeZone: "Europe/London", + month: "long", + year: "numeric", + })} +

+
+
+ ), + imageOptions + ); + } + + /** + * Generates the OpenGraph image for the leaderboard + * + * @param id the leaderboard's id + */ + public static async generateLeaderboardImage(id: string) { + const leaderboard = await this.fetchWithCache(`leaderboard-${id}`, () => + scoresaberService.lookupLeaderboard(id) + ); + if (!leaderboard) { return undefined; } const ranked = leaderboard.stars > 0; + return new ImageResponse( ( - Player's Avatar + {/* Leaderboard Cover Image */} + Leaderboard Cover + + {/* Leaderboard Name */}

{leaderboard.songName} {leaderboard.songSubName}

+
+ {/* Leaderboard Stars */} {ranked && (

{leaderboard.stars}

)} + + {/* Leaderboard Difficulty */}

{getDifficultyFromScoreSaberDifficulty(leaderboard.difficulty.difficulty)}

-

Mapped by {leaderboard.levelAuthorName}

+ + {/* Leaderboard Author */} +

Mapped by {leaderboard.levelAuthorName}

), - { - width: 1200, - height: 630, - emoji: "twemoji", - } + imageOptions ); } }