1
0

add: video metadata route for backend

This commit is contained in:
alikia2x (寒寒) 2025-10-03 16:07:27 +08:00
parent 19e2293138
commit b04a079f4d
5 changed files with 141 additions and 5 deletions

View File

@ -5,13 +5,13 @@
"name": "cvsa",
"dependencies": {
"arg": "^5.0.2",
"postgres": "^3.4.5",
"postgres": "^3.4.7",
},
"devDependencies": {
"@types/bun": "^1.2.15",
"prettier": "^3.5.3",
"@types/bun": "^1.2.22",
"prettier": "^3.6.2",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.2",
"vitest": "^3.2.4",
"vitest-tsconfig-paths": "^3.4.1",
},
},
@ -78,6 +78,7 @@
"@elysiajs/openapi": "^1.4.0",
"chalk": "^5.6.2",
"elysia": "^1.4.0",
"zod": "^4.1.11",
},
"devDependencies": {
"bun-types": "latest",
@ -2478,6 +2479,8 @@
"dot-prop/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"elysia-api/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
"execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],

View File

@ -10,7 +10,8 @@
"@elysiajs/cors": "^1.4.0",
"@elysiajs/openapi": "^1.4.0",
"chalk": "^5.6.2",
"elysia": "^1.4.0"
"elysia": "^1.4.0",
"zod": "^4.1.11"
},
"devDependencies": {
"bun-types": "latest",

View File

@ -0,0 +1,91 @@
import { Elysia, t } from "elysia";
import { dbMain } from "@core/drizzle";
import { videoSnapshot } from "@core/drizzle/main/schema";
import { bv2av } from "@elysia/lib/av_bv";
import { getVideoInfo } from "@core/net/getVideoInfo";
import { redis } from "@core/db/redis";
import { ErrorResponseSchema } from "@elysia/src/schema";
import type { VideoInfoData } from "@core/net/bilibili.d.ts";
async function retrieveFromCache(aid: number) {
const cacheKey = `cvsa:videoInfo:av${aid}`;
const cachedData = await redis.get(cacheKey);
if (cachedData) {
return JSON.parse(cachedData);
}
return null;
}
async function setCache(aid: number, data: string) {
const cacheKey = `cvsa:videoInfo:av${aid}`;
return await redis.setex(cacheKey, 60, data);
}
async function insertVideoSnapshot(data: VideoInfoData) {
const views = data.stat.view;
const danmakus = data.stat.danmaku;
const replies = data.stat.reply;
const likes = data.stat.like;
const coins = data.stat.coin;
const shares = data.stat.share;
const favorites = data.stat.favorite;
const aid = data.aid;
await dbMain.insert(videoSnapshot).values({
aid,
views,
danmakus,
replies,
likes,
coins,
shares,
favorites
});
}
export const getVideoMetadataHandler = new Elysia({ prefix: "/video" }).get(
"/:id/info",
async (c) => {
const id = c.params.id;
let aid: number | null = null;
if (id.startsWith("BV1")) {
aid = bv2av(id as `BV1${string}`);
} else if (id.startsWith("av")) {
aid = Number.parseInt(id.slice(2));
} else {
return c.status(400, {
code: "MALFORMED_SLOT",
message: "We cannot parse the video ID, or we currently do not support this format.",
errors: []
});
}
const cachedData = await retrieveFromCache(aid);
if (cachedData) {
return cachedData;
}
const data = await getVideoInfo(aid, "getVideoInfo");
if (typeof data == "number") {
return c.status(500, {
code: "THIRD_PARTY_ERROR",
message: `Got status code ${data} from bilibili API.`,
errors: []
});
}
await setCache(aid, JSON.stringify(data));
await insertVideoSnapshot(data);
return data;
},
{
response: {
200: t.Any(),
400: ErrorResponseSchema,
500: ErrorResponseSchema
}
}
);

View File

@ -5,6 +5,7 @@ import openapi from "@elysiajs/openapi";
import { cors } from "@elysiajs/cors";
import { getSongInfoHandler } from "@elysia/routes/song/info";
import { rootHandler } from "@elysia/routes/root";
import { getVideoMetadataHandler } from "@elysia/routes/video/metadata";
const [host, port] = getBindingInfo();
logStartup(host, port);
@ -20,6 +21,7 @@ const app = new Elysia({
.use(openapi())
.use(rootHandler)
.use(pingHandler)
.use(getVideoMetadataHandler)
.use(getSongInfoHandler)
.listen(15412);

View File

@ -0,0 +1,39 @@
import { t } from "elysia";
export const errorCodes = [
"INVALID_QUERY_PARAMS",
"UNKNOWN_ERROR",
"INVALID_PAYLOAD",
"MALFORMED_SLOT",
"INVALID_HEADER",
"BODY_TOO_LARGE",
"UNAUTHORIZED",
"INVALID_CREDENTIALS",
"ENTITY_NOT_FOUND",
"SERVER_ERROR",
"RATE_LIMIT_EXCEEDED",
"ENTITY_EXISTS",
"THIRD_PARTY_ERROR"
];
function generateErrorCodeRegex(strings: string[]): string {
if (strings.length === 0) {
return "(?!)";
}
const escapedStrings = strings.map((str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
return `^(${escapedStrings.join("|")})$`;
}
export const ErrorResponseSchema = t.Object({
code: t.String({ pattern: generateErrorCodeRegex(errorCodes) }),
message: t.String(),
errors: t.Array(t.String()),
i18n: t.Optional(
t.Object({
key: t.String(),
values: t.Optional(t.Record(t.String(), t.Union([t.String(), t.Number(), t.Date()])))
})
)
});