re-add per page/leaderboard embed colors
All checks were successful
Deploy Backend / deploy (push) Successful in 3m31s
Deploy Website / deploy (push) Successful in 5m17s

This commit is contained in:
Lee
2024-10-16 07:31:52 +01:00
parent 7f42a27d8f
commit ff9408fb8c
12 changed files with 131 additions and 22 deletions

View File

@ -1,5 +1,18 @@
FROM oven/bun:1.1.30-alpine AS base
# Install system dependencies for node-canvas
RUN apk add --no-cache \
build-base \
cairo-dev \
pango-dev \
giflib-dev \
libjpeg-turbo-dev \
freetype-dev \
fontconfig-dev \
pixman-dev \
python3 \
pkgconfig
# Install dependencies
FROM base AS depends
WORKDIR /app
@ -24,4 +37,4 @@ RUN bun --filter '@ssr/common' build
# Copy the backend project
COPY --from=depends /app/projects/backend ./projects/backend
CMD ["bun", "run", "--filter", "backend", "start"]
CMD ["bun", "run", "--filter", "backend", "start"]

View File

@ -15,12 +15,14 @@
"@tqman/nice-logger": "^1.0.1",
"@typegoose/typegoose": "^12.8.0",
"@vercel/og": "^0.6.3",
"canvas": "^3.0.0-rc2",
"discord-webhook-node": "^1.1.8",
"elysia": "latest",
"elysia-autoroutes": "^0.5.0",
"elysia-decorators": "^1.0.2",
"elysia-helmet": "^2.0.0",
"elysia-rate-limit": "^4.1.0",
"extract-colors": "^4.1.0",
"ky": "^1.7.2",
"mongoose": "^8.7.0",
"node-cache": "^5.1.2",

View File

@ -2,4 +2,5 @@ export const Config = {
mongoUri: process.env.MONGO_URI,
apiUrl: process.env.API_URL || "https://ssr.fascinated.cc/api",
trackedPlayerWebhook: process.env.TRACKED_PLAYERS_WEBHOOK,
numberOneWebhook: process.env.NUMBER_ONE_WEBHOOK,
};

View File

@ -4,6 +4,16 @@ import { ImageService } from "../service/image.service";
@Controller("/image")
export default class ImageController {
@Get("/averagecolor/:url", {
config: {},
params: t.Object({
url: t.String({ required: true }),
}),
})
public async getImageAverageColor({ params: { url } }: { params: { url: string } }) {
return await ImageService.getAverageImageColor(url);
}
@Get("/player/:id", {
config: {},
params: t.Object({

View File

@ -21,6 +21,10 @@ import { delay } from "@ssr/common/utils/utils";
import { connectScoreSaberWebSocket } from "@ssr/common/websocket/scoresaber-websocket";
import ImageController from "./controller/image.controller";
import ReplayController from "./controller/replay.controller";
// @ts-ignore
import { MessageBuilder, Webhook } from "discord-webhook-node";
import { formatPp } from "@ssr/common/utils/number-utils";
import { ScoreService } from "./service/score.service";
// Load .env file
dotenv.config({
@ -33,8 +37,9 @@ await mongoose.connect(Config.mongoUri!); // Connect to MongoDB
setLogLevel("DEBUG");
connectScoreSaberWebSocket({
onScore: async score => {
await PlayerService.trackScore(score);
onScore: async playerScore => {
await PlayerService.trackScore(playerScore);
await ScoreService.notifyNumberOne(playerScore);
},
});

View File

@ -9,11 +9,58 @@ import NodeCache from "node-cache";
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";
import ky from "ky";
import { createCanvas, loadImage } from "canvas";
import { extractColors } from "extract-colors";
const cache = new NodeCache({ stdTTL: 60 * 60, checkperiod: 120 });
const imageOptions = { width: 1200, height: 630 };
export class ImageService {
/**
* Gets the average color of an image
*
* @param src the image url
* @returns the average color
* @private
*/
public static async getAverageImageColor(src: string): Promise<{ color: string } | undefined> {
src = decodeURIComponent(src);
return await this.fetchWithCache<{ color: string }>(`average_color-${src}`, async () => {
try {
const response = await ky.get(src);
if (response.status !== 200) {
throw new Error(`Failed to fetch image: ${src}`);
}
const imageBuffer = await response.arrayBuffer();
// Create an image from the buffer using canvas
const img = await loadImage(Buffer.from(imageBuffer));
const canvas = createCanvas(img.width, img.height);
const ctx = canvas.getContext("2d");
// Draw the image onto the canvas
ctx.drawImage(img, 0, 0);
// Get the pixel data from the canvas
const imageData = ctx.getImageData(0, 0, img.width, img.height);
const { data, width, height } = imageData;
// Extract the colors
const color = await extractColors({ data, width, height });
return {
color: color[2].hex,
};
} catch (error) {
return {
color: "#fff",
};
}
});
}
/**
* Fetches data with caching.
*

View File

@ -0,0 +1,36 @@
import ScoreSaberPlayerScoreToken from "@ssr/common/types/token/scoresaber/score-saber-player-score-token";
// @ts-ignore
import { MessageBuilder, Webhook } from "discord-webhook-node";
import { Config } from "../common/config";
import { formatPp } from "@ssr/common/utils/number-utils";
export class ScoreService {
public static async notifyNumberOne(playerScore: ScoreSaberPlayerScoreToken) {
const { score, leaderboard } = playerScore;
const player = score.leaderboardPlayerInfo;
// Not ranked
if (leaderboard.stars <= 0) {
return;
}
// Not #1 rank
if (score.rank !== 1) {
return;
}
const hook = new Webhook({
url: Config.numberOneWebhook,
});
hook.setUsername("Number One Feed");
const embed = new MessageBuilder();
embed.setTitle(`${player.name} set a #${score.rank} on ${leaderboard.songName} ${leaderboard.songSubName}`);
embed.setDescription(`
**Player:** https://ssr.fascinated.cc/player/${player.id}
**Leaderboard:** https://ssr.fascinated.cc/leaderboard/${leaderboard.id}
**PP:** ${formatPp(score.pp)}
`);
embed.setThumbnail(leaderboard.coverImage);
embed.setColor("#00ff00");
await hook.send(embed);
}
}

View File

@ -103,14 +103,8 @@ export async function generateViewport(props: Props): Promise<Viewport> {
}
const color = await getAverageColor(leaderboard.coverImage);
if (color === undefined) {
return {
themeColor: Colors.primary,
};
}
return {
themeColor: color?.hex,
themeColor: color.hex,
};
}

View File

@ -117,14 +117,8 @@ export async function generateViewport(props: Props): Promise<Viewport> {
}
const color = await getAverageColor(player.avatar);
if (color === undefined) {
return {
themeColor: Colors.primary,
};
}
return {
themeColor: color?.hex,
themeColor: color.hex,
};
}

View File

@ -1,4 +1,6 @@
import { config } from "../../config";
import ky from "ky";
import { Colors } from "@/common/colors";
/**
* Proxies all non-localhost images to make them load faster.
@ -17,7 +19,11 @@ export function getImageUrl(originalUrl: string) {
* @returns the average color
*/
export const getAverageColor = async (src: string) => {
return {
hex: "#fff",
};
try {
return await ky.get<{ hex: string }>(`${config.siteApi}/image/averagecolor/${encodeURIComponent(src)}`).json();
} catch {
return {
hex: Colors.primary,
};
}
};