re-add per page/leaderboard embed colors
This commit is contained in:
@ -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"]
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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({
|
||||
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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.
|
||||
*
|
||||
|
36
projects/backend/src/service/score.service.ts
Normal file
36
projects/backend/src/service/score.service.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user