From 71ed0bd66ba8bf35d39481c8755966de5e899c43 Mon Sep 17 00:00:00 2001 From: alikia2x Date: Mon, 7 Apr 2025 21:29:53 +0800 Subject: [PATCH] feat: videoInfo API in backend --- packages/backend/database.ts | 6 ++--- packages/backend/deno.json | 7 ++--- packages/backend/main.ts | 13 ++++++---- packages/backend/register.ts | 10 +++---- packages/backend/root.ts | 22 +++++++--------- packages/backend/singers.ts | 2 +- packages/backend/snapshots.ts | 9 +++---- packages/backend/utils.ts | 4 +-- packages/backend/videoInfo.ts | 39 ++++++++++++++++++++++++++++ packages/crawler/net/getVideoInfo.ts | 24 +++++++++++++++++ 10 files changed, 100 insertions(+), 36 deletions(-) create mode 100644 packages/backend/videoInfo.ts diff --git a/packages/backend/database.ts b/packages/backend/database.ts index 678cee6..fdd67ec 100644 --- a/packages/backend/database.ts +++ b/packages/backend/database.ts @@ -9,18 +9,18 @@ export const db = pool; export const dbCred = poolCred; export const dbMiddleware = createMiddleware(async (c, next) => { - const connection = await pool.connect(); + const connection = await pool.connect(); c.set("db", connection); await next(); connection.release(); }); export const dbCredMiddleware = createMiddleware(async (c, next) => { - const connection = await poolCred.connect(); + const connection = await poolCred.connect(); c.set("dbCred", connection); await next(); connection.release(); -}) +}); declare module "hono" { interface ContextVariableMap { diff --git a/packages/backend/deno.json b/packages/backend/deno.json index 9d3fe45..df5ca5d 100644 --- a/packages/backend/deno.json +++ b/packages/backend/deno.json @@ -4,11 +4,12 @@ "@rabbit-company/argon2id": "jsr:@rabbit-company/argon2id@^2.1.0", "hono": "jsr:@hono/hono@^4.7.5", "zod": "npm:zod", - "yup": "npm:yup" + "yup": "npm:yup", + "@crawler/net/videoInfo": "../crawler/net/getVideoInfo.ts" }, "tasks": { - "dev": "deno serve --env-file=.env --allow-env --allow-net --watch main.ts", - "start": "deno serve --env-file=.env --allow-env --allow-net --host 127.0.0.1 main.ts" + "dev": "deno serve --env-file=.env --allow-env --allow-net --allow-read --allow-write --allow-run --watch main.ts", + "start": "deno serve --env-file=.env --allow-env --allow-net --allow-read --allow-write --allow-run --host 127.0.0.1 main.ts" }, "compilerOptions": { "jsx": "precompile", diff --git a/packages/backend/main.ts b/packages/backend/main.ts index bb41e21..b9e5b72 100644 --- a/packages/backend/main.ts +++ b/packages/backend/main.ts @@ -3,16 +3,19 @@ import { dbCredMiddleware, dbMiddleware } from "./database.ts"; import { rootHandler } from "./root.ts"; import { getSnapshotsHanlder } from "./snapshots.ts"; import { registerHandler } from "./register.ts"; +import { videoInfoHandler } from "./videoInfo.ts"; export const app = new Hono(); -app.use('/video/*', dbMiddleware); -app.use('/user', dbCredMiddleware); +app.use("/video/*", dbMiddleware); +app.use("/user", dbCredMiddleware); app.get("/", ...rootHandler); -app.get('/video/:id/snapshots', ...getSnapshotsHanlder); -app.post('/user', ...registerHandler); +app.get("/video/:id/snapshots", ...getSnapshotsHanlder); +app.post("/user", ...registerHandler); + +app.get("/video/:id/info", ...videoInfoHandler); const fetch = app.fetch; @@ -20,4 +23,4 @@ export default { fetch, } satisfies Deno.ServeDefaultExport; -export const VERSION = "0.3.0"; \ No newline at end of file +export const VERSION = "0.4.0"; diff --git a/packages/backend/register.ts b/packages/backend/register.ts index 401ef6b..7964668 100644 --- a/packages/backend/register.ts +++ b/packages/backend/register.ts @@ -8,7 +8,7 @@ import type { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts"; const RegistrationBodySchema = object({ username: string().trim().required("Username is required").max(50, "Username cannot exceed 50 characters"), password: string().required("Password is required"), - nickname: string().optional(), + nickname: string().optional(), }); type ContextType = Context; @@ -19,7 +19,7 @@ export const userExists = async (username: string, client: Client) => { `; const result = await client.queryObject(query, [username]); return result.rows.length > 0; -} +}; export const registerHandler = createHandlers(async (c: ContextType) => { const client = c.get("dbCred"); @@ -28,11 +28,11 @@ export const registerHandler = createHandlers(async (c: ContextType) => { const body = await RegistrationBodySchema.validate(await c.req.json()); const { username, password, nickname } = body; - if (await userExists(username, client)) { + if (await userExists(username, client)) { return c.json({ message: `User "${username}" already exists.`, }, 400); - } + } const hash = await Argon2id.hashEncoded(password); @@ -49,7 +49,7 @@ export const registerHandler = createHandlers(async (c: ContextType) => { return c.json({ message: "Invalid registration data.", errors: e.errors, - }, 400); + }, 400); } else if (e instanceof SyntaxError) { return c.json({ message: "Invalid JSON in request body.", diff --git a/packages/backend/root.ts b/packages/backend/root.ts index 3cb15fd..6fe5d65 100644 --- a/packages/backend/root.ts +++ b/packages/backend/root.ts @@ -5,27 +5,25 @@ import { createHandlers } from "./utils.ts"; export const rootHandler = createHandlers((c) => { let singer: Singer | Singer[] | null = null; const shouldShowSpecialSinger = Math.random() < 0.016; - if (getSingerForBirthday().length !== 0){ + if (getSingerForBirthday().length !== 0) { singer = getSingerForBirthday(); for (const s of singer) { delete s.birthday; - s.message = `祝${s.name}生日快乐~` + s.message = `祝${s.name}生日快乐~`; } - } - else if (shouldShowSpecialSinger) { - singer = pickSpecialSinger(); - } - else { - singer = pickSinger(); + } else if (shouldShowSpecialSinger) { + singer = pickSpecialSinger(); + } else { + singer = pickSinger(); } return c.json({ "project": { "name": "中V档案馆", - "motto": "一起唱吧,心中的歌!" + "motto": "一起唱吧,心中的歌!", }, "status": 200, "version": VERSION, "time": Date.now(), - "singer": singer - }) -}) \ No newline at end of file + "singer": singer, + }); +}); diff --git a/packages/backend/singers.ts b/packages/backend/singers.ts index 47963bb..a24abb9 100644 --- a/packages/backend/singers.ts +++ b/packages/backend/singers.ts @@ -70,7 +70,7 @@ export interface Singer { name: string; color?: string; birthday?: string; - message?: string; + message?: string; } export const specialSingers = [ diff --git a/packages/backend/snapshots.ts b/packages/backend/snapshots.ts index 27a2095..7d191d5 100644 --- a/packages/backend/snapshots.ts +++ b/packages/backend/snapshots.ts @@ -12,12 +12,12 @@ const SnapshotQueryParamsSchema = object({ reverse: boolean().optional(), }); -const idSchema = mixed().test( +export const idSchema = mixed().test( "is-valid-id", 'id must be a string starting with "av" followed by digits, or "BV" followed by 10 alphanumeric characters, or a positive integer', async (value) => { if (value && await number().integer().isValid(value)) { - const v = parseInt(value as string); + const v = parseInt(value as string); return Number.isInteger(v) && v > 0; } @@ -46,10 +46,9 @@ export const getSnapshotsHanlder = createHandlers(async (c: ContextType) => { let videoId: string | number = idParam as string; if (videoId.startsWith("av")) { videoId = parseInt(videoId.slice(2)); - } - else if (await number().isValid(videoId)) { + } else if (await number().isValid(videoId)) { videoId = parseInt(videoId); - } + } const queryParams = await SnapshotQueryParamsSchema.validate(c.req.query()); const { ps, pn, offset, reverse = false } = queryParams; diff --git a/packages/backend/utils.ts b/packages/backend/utils.ts index 9d06a7c..053da35 100644 --- a/packages/backend/utils.ts +++ b/packages/backend/utils.ts @@ -1,5 +1,5 @@ -import { createFactory } from 'hono/factory' +import { createFactory } from "hono/factory"; const factory = createFactory(); -export const createHandlers = factory.createHandlers; \ No newline at end of file +export const createHandlers = factory.createHandlers; diff --git a/packages/backend/videoInfo.ts b/packages/backend/videoInfo.ts new file mode 100644 index 0000000..351b2de --- /dev/null +++ b/packages/backend/videoInfo.ts @@ -0,0 +1,39 @@ +import type { Context } from "hono"; +import { createHandlers } from "./utils.ts"; +import type { BlankEnv, BlankInput } from "hono/types"; +import { number, ValidationError } from "yup"; +import { getVideoInfo, getVideoInfoByBV } from "@crawler/net/videoInfo"; +import { idSchema } from "./snapshots.ts"; +import type { VideoInfoData } from "../crawler/net/bilibili.d.ts"; + +type ContextType = Context; + +export const videoInfoHandler = createHandlers(async (c: ContextType) => { + try { + const id = await idSchema.validate(c.req.param("id")); + let videoId: string | number = id as string; + if (videoId.startsWith("av")) { + videoId = parseInt(videoId.slice(2)); + } else if (await number().isValid(videoId)) { + videoId = parseInt(videoId); + } + let result: VideoInfoData | number; + if (typeof videoId === "number") { + result = await getVideoInfo(videoId, "getVideoInfo"); + } else { + result = await getVideoInfoByBV(videoId, "getVideoInfo"); + } + + if (typeof result === "number") { + return c.json({ message: "Error fetching video info", code: result }, 500); + } + + return c.json(result); + } catch (e) { + if (e instanceof ValidationError) { + return c.json({ message: "Invalid query parameters", errors: e.errors }, 400); + } else { + return c.json({ message: "Unhandled error", error: e }, 500); + } + } +}); diff --git a/packages/crawler/net/getVideoInfo.ts b/packages/crawler/net/getVideoInfo.ts index 0533c53..8a71edd 100644 --- a/packages/crawler/net/getVideoInfo.ts +++ b/packages/crawler/net/getVideoInfo.ts @@ -25,3 +25,27 @@ export async function getVideoInfo(aid: number, task: string): Promise} VideoInfoData or the error code returned by bilibili API + * @throws {NetSchedulerError} - The error will be thrown in following cases: + * - No proxy is available currently: with error code `NO_PROXY_AVAILABLE` + * - The native `fetch` function threw an error: with error code `FETCH_ERROR` + * - The alicloud-fc threw an error: with error code `ALICLOUD_FC_ERROR` + */ +export async function getVideoInfoByBV(bvid: string, task: string): Promise { + const url = `https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`; + const data = await netScheduler.request(url, task); + const errMessage = `Error fetching metadata for ${bvid}:`; + if (data.code !== 0) { + logger.error(errMessage + data.code + "-" + data.message, "net", "fn:getVideoInfoByBV"); + return data.code; + } + return data.data; +} \ No newline at end of file