add: video metadata route for backend
This commit is contained in:
parent
19e2293138
commit
b04a079f4d
11
bun.lock
11
bun.lock
@ -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=="],
|
||||
|
||||
@ -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",
|
||||
|
||||
91
packages/elysia/routes/video/metadata.ts
Normal file
91
packages/elysia/routes/video/metadata.ts
Normal 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
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -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);
|
||||
|
||||
|
||||
39
packages/elysia/src/schema.ts
Normal file
39
packages/elysia/src/schema.ts
Normal 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()])))
|
||||
})
|
||||
)
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user