merge: branch 'ref/deno' into ref/docker

This commit is contained in:
alikia2x (寒寒) 2025-04-29 04:00:57 +08:00
commit 784939074a
Signed by: alikia2x
GPG Key ID: 56209E0CCD8420C6
35 changed files with 575 additions and 178 deletions

6
.idea/bun.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="BunSettings">
<option name="bunPath" value="$USER_HOME$/.bun/bin/bun" />
</component>
</project>

View File

@ -0,0 +1,55 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" />
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

6
.idea/compiler.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="TypeScriptCompiler">
<option name="useTypesFromServer" value="true" />
</component>
</project>

View File

@ -2,5 +2,6 @@
<project version="4"> <project version="4">
<component name="DenoSettings"> <component name="DenoSettings">
<option name="denoInit" value="{&#10; &quot;enable&quot;: true,&#10; &quot;lint&quot;: true,&#10; &quot;unstable&quot;: true,&#10; &quot;importMap&quot;: &quot;import_map.json&quot;,&#10; &quot;config&quot;: &quot;deno.json&quot;,&#10; &quot;fmt&quot;: {&#10; &quot;useTabs&quot;: true,&#10; &quot;lineWidth&quot;: 120,&#10; &quot;indentWidth&quot;: 4,&#10; &quot;semiColons&quot;: true,&#10; &quot;proseWrap&quot;: &quot;always&quot;&#10; }&#10;}" /> <option name="denoInit" value="{&#10; &quot;enable&quot;: true,&#10; &quot;lint&quot;: true,&#10; &quot;unstable&quot;: true,&#10; &quot;importMap&quot;: &quot;import_map.json&quot;,&#10; &quot;config&quot;: &quot;deno.json&quot;,&#10; &quot;fmt&quot;: {&#10; &quot;useTabs&quot;: true,&#10; &quot;lineWidth&quot;: 120,&#10; &quot;indentWidth&quot;: 4,&#10; &quot;semiColons&quot;: true,&#10; &quot;proseWrap&quot;: &quot;always&quot;&#10; }&#10;}" />
<option name="useDenoValue" value="DISABLE" />
</component> </component>
</project> </project>

View File

@ -31,5 +31,6 @@
<option name="processLiterals" value="true" /> <option name="processLiterals" value="true" />
<option name="processComments" value="true" /> <option name="processComments" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="TsLint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile> </profile>
</component> </component>

View File

@ -0,0 +1,8 @@
{
"useTabs": true,
"tabWidth": 4,
"trailingComma": "none",
"singleQuote": false,
"printWidth": 120,
"endOfLine": "lf"
}

66
packages/backend/bun.lock Normal file
View File

@ -0,0 +1,66 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": {
"@rabbit-company/argon2id": "^2.1.0",
"hono": "^4.7.8",
"hono-rate-limiter": "^0.4.2",
"ioredis": "^5.6.1",
"postgres": "^3.4.5",
"yup": "^1.6.1",
"zod": "^3.24.3",
},
"devDependencies": {
"prettier": "^3.5.3",
},
},
},
"packages": {
"@ioredis/commands": ["@ioredis/commands@1.2.0", "", {}, "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="],
"@rabbit-company/argon2id": ["@rabbit-company/argon2id@2.1.0", "", { "peerDependencies": { "typescript": "^5.6.2" } }, "sha512-X/kt89qjmS9+Zh+DYCGcWeTwHa4C8vY8T3EnSma+vWj7spMzAYX4F8vmGUkny9hygpTOeC/yXwAUdJAfJ52H+w=="],
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
"hono": ["hono@4.7.8", "", {}, "sha512-PCibtFdxa7/Ldud9yddl1G81GjYaeMYYTq4ywSaNsYbB1Lug4mwtOMJf2WXykL0pntYwmpRJeOI3NmoDgD+Jxw=="],
"hono-rate-limiter": ["hono-rate-limiter@0.4.2", "", { "peerDependencies": { "hono": "^4.1.1" } }, "sha512-AAtFqgADyrmbDijcRTT/HJfwqfvhalya2Zo+MgfdrMPas3zSMD8SU03cv+ZsYwRU1swv7zgVt0shwN059yzhjw=="],
"ioredis": ["ioredis@5.6.1", "", { "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA=="],
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"postgres": ["postgres@3.4.5", "", {}, "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg=="],
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
"property-expr": ["property-expr@2.0.6", "", {}, "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="],
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
"tiny-case": ["tiny-case@1.0.3", "", {}, "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="],
"toposort": ["toposort@2.0.2", "", {}, "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="],
"type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"yup": ["yup@1.6.1", "", { "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", "toposort": "^2.0.2", "type-fest": "^2.19.0" } }, "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA=="],
"zod": ["zod@3.24.3", "", {}, "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg=="],
}
}

View File

@ -1,30 +0,0 @@
import { type Client, Pool } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
import { postgresConfig, postgresConfigCred } from "@core/db/pgConfig.ts";
import { createMiddleware } from "hono/factory";
const pool = new Pool(postgresConfig, 4);
const poolCred = new Pool(postgresConfigCred, 2);
export const db = pool;
export const dbCred = poolCred;
export const dbMiddleware = createMiddleware(async (c, next) => {
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();
c.set("dbCred", connection);
await next();
connection.release();
});
declare module "hono" {
interface ContextVariableMap {
db: Client;
dbCred: Client;
}
}

View File

@ -0,0 +1,38 @@
const requiredEnvVars = ["DB_HOST", "DB_NAME", "DB_USER", "DB_PASSWORD", "DB_PORT", "DB_NAME_CRED"];
const unsetVars = requiredEnvVars.filter((key) => process.env[key] === undefined);
if (unsetVars.length > 0) {
throw new Error(`Missing required environment variables: ${unsetVars.join(", ")}`);
}
const databaseHost = process.env["DB_HOST"]!;
const databaseName = process.env["DB_NAME"];
const databaseNameCred = process.env["DB_NAME_CRED"]!;
const databaseUser = process.env["DB_USER"]!;
const databasePassword = process.env["DB_PASSWORD"]!;
const databasePort = process.env["DB_PORT"]!;
export const postgresConfig = {
hostname: databaseHost,
port: parseInt(databasePort),
database: databaseName,
user: databaseUser,
password: databasePassword
};
export const postgresConfigNpm = {
host: databaseHost,
port: parseInt(databasePort),
database: databaseName,
username: databaseUser,
password: databasePassword
};
export const postgresConfigCred = {
hostname: databaseHost,
port: parseInt(databasePort),
database: databaseNameCred,
user: databaseUser,
password: databasePassword
};

View File

@ -0,0 +1,6 @@
import postgres from "postgres";
import { postgresConfigNpm } from "./config";
const sql = postgres(postgresConfigNpm);
export default sql;

View File

@ -0,0 +1,58 @@
import sql from "./db";
import type { VideoSnapshotType } from "@core/db/schema.d.ts";
export async function getVideoSnapshots(
aid: number,
limit: number,
pageOrOffset: number,
reverse: boolean,
mode: "page" | "offset" = "page"
) {
const offset = mode === "page" ? (pageOrOffset - 1) * limit : pageOrOffset;
if (reverse) {
return sql<VideoSnapshotType[]>`
SELECT *
FROM video_snapshot
WHERE aid = ${aid}
ORDER BY created_at
LIMIT ${limit} OFFSET ${offset}
`;
} else {
return sql<VideoSnapshotType[]>`
SELECT *
FROM video_snapshot
WHERE aid = ${aid}
ORDER BY created_at DESC
LIMIT ${limit} OFFSET ${offset}
`;
}
}
export async function getVideoSnapshotsByBV(
bv: string,
limit: number,
pageOrOffset: number,
reverse: boolean,
mode: "page" | "offset" = "page"
) {
const offset = mode === "page" ? (pageOrOffset - 1) * limit : pageOrOffset;
if (reverse) {
return sql<VideoSnapshotType[]>`
SELECT vs.*
FROM video_snapshot vs
JOIN bilibili_metadata bm ON vs.aid = bm.aid
WHERE bm.bvid = ${bv}
ORDER BY vs.created_at
LIMIT ${limit} OFFSET ${offset}
`;
} else {
return sql<VideoSnapshotType[]>`
SELECT vs.*
FROM video_snapshot vs
JOIN bilibili_metadata bm ON vs.aid = bm.aid
WHERE bm.bvid = ${bv}
ORDER BY vs.created_at DESC
LIMIT ${limit} OFFSET ${offset}
`;
}
}

View File

@ -1,22 +0,0 @@
{
"name": "@cvsa/backend",
"imports": {
"@rabbit-company/argon2id": "jsr:@rabbit-company/argon2id@^2.1.0",
"hono": "jsr:@hono/hono@^4.7.5",
"zod": "npm:zod",
"yup": "npm:yup",
"@core/": "../core/",
"log/": "../core/log/",
"@crawler/net/videoInfo": "../crawler/net/getVideoInfo.ts",
"ioredis": "npm:ioredis"
},
"tasks": {
"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",
"jsxImportSource": "hono/jsx"
},
"exports": "./main.ts"
}

View File

@ -1,26 +1,39 @@
import { Hono } from "hono"; import { Hono } from "hono";
import { dbCredMiddleware, dbMiddleware } from "./database.ts";
import { rootHandler } from "./root.ts"; import { rootHandler } from "./root.ts";
import { getSnapshotsHanlder } from "./snapshots.ts"; import { getSnapshotsHanlder } from "./snapshots.ts";
import { registerHandler } from "./register.ts"; import { registerHandler } from "./register.ts";
import { videoInfoHandler } from "./videoInfo.ts"; import { videoInfoHandler } from "./videoInfo.ts";
import { pingHandler } from "./ping.ts";
// import { getConnInfo } from "hono/deno";
//import { rateLimiter } from "hono-rate-limiter";
// import { MINUTE } from "https://deno.land/std@0.216.0/datetime/constants.ts";
// import type { Context } from "hono";
// import type { BlankEnv } from "hono/types";
export const app = new Hono(); export const app = new Hono();
app.use("/video/*", dbMiddleware); // const limiter = rateLimiter<BlankEnv, "/user", {}>({
app.use("/user", dbCredMiddleware); // windowMs: 60 * MINUTE,
// limit: 5,
// standardHeaders: "draft-6",
// keyGenerator: (c) => {
// const info = getConnInfo(c as unknown as Context<BlankEnv, "/user", {}>);
// if (!info.remote || !info.remote.address) {
// return crypto.randomUUID()
// }
// return info.remote.address;
// },
// });
// app.use("/user", limiter);
app.get("/", ...rootHandler); app.get("/", ...rootHandler);
app.get("/ping", ...pingHandler);
app.get("/video/:id/snapshots", ...getSnapshotsHanlder); app.get("/video/:id/snapshots", ...getSnapshotsHanlder);
app.post("/user", ...registerHandler); app.post("/user", ...registerHandler);
app.get("/video/:id/info", ...videoInfoHandler); app.get("/video/:id/info", ...videoInfoHandler);
const fetch = app.fetch; export default app
export default {
fetch,
} satisfies Deno.ServeDefaultExport;
export const VERSION = "0.4.2"; export const VERSION = "0.4.2";

View File

@ -0,0 +1,19 @@
{
"scripts": {
"format": "prettier --write .",
"dev": "bun run --hot main.ts"
},
"dependencies": {
"@rabbit-company/argon2id": "^2.1.0",
"hono": "^4.7.8",
"hono-rate-limiter": "^0.4.2",
"ioredis": "^5.6.1",
"postgres": "^3.4.5",
"yup": "^1.6.1",
"zod": "^3.24.3"
},
"devDependencies": {
"prettier": "^3.5.3",
"@types/bun": "latest"
}
}

7
packages/backend/ping.ts Normal file
View File

@ -0,0 +1,7 @@
import { createHandlers } from "./utils.ts";
export const pingHandler = createHandlers(async (c) => {
return c.json({
"message": "pong"
});
});

View File

@ -3,63 +3,72 @@ import Argon2id from "@rabbit-company/argon2id";
import { object, string, ValidationError } from "yup"; import { object, string, ValidationError } from "yup";
import type { Context } from "hono"; import type { Context } from "hono";
import type { Bindings, BlankEnv, BlankInput } from "hono/types"; import type { Bindings, BlankEnv, BlankInput } from "hono/types";
import type { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts"; import sql from "./db/db.ts";
import { ErrorResponse, StatusResponse } from "./schema";
const RegistrationBodySchema = object({ const RegistrationBodySchema = object({
username: string().trim().required("Username is required").max(50, "Username cannot exceed 50 characters"), username: string().trim().required("Username is required").max(50, "Username cannot exceed 50 characters"),
password: string().required("Password is required"), password: string().required("Password is required"),
nickname: string().optional(), nickname: string().optional()
}); });
type ContextType = Context<BlankEnv & { Bindings: Bindings }, "/user", BlankInput>; type ContextType = Context<BlankEnv & { Bindings: Bindings }, "/user", BlankInput>;
export const userExists = async (username: string, client: Client) => { export const userExists = async (username: string) => {
const query = ` const result = await sql`
SELECT * FROM users WHERE username = $1 SELECT 1
FROM users
WHERE username = ${username}
`; `;
const result = await client.queryObject(query, [username]); return result.length > 0;
return result.rows.length > 0;
}; };
export const registerHandler = createHandlers(async (c: ContextType) => { export const registerHandler = createHandlers(async (c: ContextType) => {
const client = c.get("dbCred");
try { try {
const body = await RegistrationBodySchema.validate(await c.req.json()); const body = await RegistrationBodySchema.validate(await c.req.json());
const { username, password, nickname } = body; const { username, password, nickname } = body;
if (await userExists(username, client)) { if (await userExists(username)) {
return c.json({ const response: StatusResponse = {
message: `User "${username}" already exists.`, message: `User "${username}" already exists.`
}, 400); };
return c.json<StatusResponse>(response, 400);
} }
const hash = await Argon2id.hashEncoded(password); const hash = await Argon2id.hashEncoded(password);
const query = ` await sql`
INSERT INTO users (username, password, nickname) VALUES ($1, $2, $3) INSERT INTO users (username, password, nickname)
VALUES (${username}, ${hash}, ${nickname ? nickname : null})
`; `;
await client.queryObject(query, [username, hash, nickname || null]);
return c.json({ const response: StatusResponse = {
message: `User "${username}" registered successfully.`, message: `User "${username}" registered successfully.`
}, 201); }
return c.json<StatusResponse>(response, 201);
} catch (e) { } catch (e) {
if (e instanceof ValidationError) { if (e instanceof ValidationError) {
return c.json({ const response: ErrorResponse<string> = {
message: "Invalid registration data.", message: "Invalid registration data.",
errors: e.errors, errors: e.errors,
}, 400); code: "INVALID_PAYLOAD"
}
return c.json<ErrorResponse<string>>(response, 400);
} else if (e instanceof SyntaxError) { } else if (e instanceof SyntaxError) {
return c.json({ const response: ErrorResponse<string> = {
message: "Invalid JSON in request body.", message: "Invalid JSON payload.",
}, 400); errors: [e.message],
code: "INVALID_FORMAT"
}
return c.json<ErrorResponse<string>>(response, 400);
} else { } else {
console.error("Registration error:", e); const response: ErrorResponse<string> = {
return c.json({ message: "Invalid JSON payload.",
message: "An unexpected error occurred during registration.", errors: [(e as Error).message],
error: (e as Error).message, code: "UNKNOWN_ERR"
}, 500); }
return c.json<ErrorResponse<string>>(response, 500);
} }
} }
}); });

View File

@ -17,13 +17,13 @@ export const rootHandler = createHandlers((c) => {
singer = pickSinger(); singer = pickSinger();
} }
return c.json({ return c.json({
"project": { project: {
"name": "中V档案馆", name: "中V档案馆",
"motto": "一起唱吧,心中的歌!", motto: "一起唱吧,心中的歌!"
}, },
"status": 200, status: 200,
"version": VERSION, version: VERSION,
"time": Date.now(), time: Date.now(),
"singer": singer, singer: singer
}); });
}); });

11
packages/backend/schema.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
type ErrorCode = "INVALID_QUERY_PARAMS" | "UNKNOWN_ERR" | "INVALID_PAYLOAD" | "INVALID_FORMAT";
export interface ErrorResponse<E> {
code: ErrorCode
message: string;
errors: E[];
}
export interface StatusResponse {
message: string;
}

View File

@ -1,69 +1,69 @@
export const singers = [ export const singers = [
{ {
"name": "洛天依", name: "洛天依",
"color": "#66CCFF", color: "#66CCFF",
"birthday": "0712", birthday: "0712"
}, },
{ {
"name": "言和", name: "言和",
"color": "#00FFCC", color: "#00FFCC",
"birthday": "0711", birthday: "0711"
}, },
{ {
"name": "乐正绫", name: "乐正绫",
"color": "#EE0000", color: "#EE0000",
"birthday": "0412", birthday: "0412"
}, },
{ {
"name": "乐正龙牙", name: "乐正龙牙",
"color": "#006666", color: "#006666",
"birthday": "1002", birthday: "1002"
}, },
{ {
"name": "徵羽摩柯", name: "徵羽摩柯",
"color": "#0080FF", color: "#0080FF",
"birthday": "1210", birthday: "1210"
}, },
{ {
"name": "墨清弦", name: "墨清弦",
"color": "#FFFF00", color: "#FFFF00",
"birthday": "0520", birthday: "0520"
}, },
{ {
"name": "星尘", name: "星尘",
"color": "#9999FF", color: "#9999FF",
"birthday": "0812", birthday: "0812"
}, },
{ {
"name": "心华", name: "心华",
"color": "#EE82EE", color: "#EE82EE",
"birthday": "0210", birthday: "0210"
}, },
{ {
"name": "海伊", name: "海伊",
"color": "#3399FF", color: "#3399FF",
"birthday": "0722", birthday: "0722"
}, },
{ {
"name": "苍穹", name: "苍穹",
"color": "#8BC0B5", color: "#8BC0B5",
"birthday": "0520", birthday: "0520"
}, },
{ {
"name": "赤羽", name: "赤羽",
"color": "#FF4004", color: "#FF4004",
"birthday": "1126", birthday: "1126"
}, },
{ {
"name": "诗岸", name: "诗岸",
"color": "#F6BE72", color: "#F6BE72",
"birthday": "0119", birthday: "0119"
}, },
{ {
"name": "牧心", name: "牧心",
"color": "#2A2859", color: "#2A2859",
"birthday": "0807", birthday: "0807"
}, }
]; ];
export interface Singer { export interface Singer {
@ -75,13 +75,13 @@ export interface Singer {
export const specialSingers = [ export const specialSingers = [
{ {
"name": "雅音宫羽", name: "雅音宫羽",
"message": "你是我最真模样,从来不曾遗忘。", message: "你是我最真模样,从来不曾遗忘。"
}, },
{ {
"name": "初音未来", name: "初音未来",
"message": "初始之音,响彻未来!", message: "初始之音,响彻未来!"
}, }
]; ];
export const pickSinger = () => { export const pickSinger = () => {

View File

@ -1,22 +1,23 @@
import type { Context } from "hono"; import type { Context } from "hono";
import { createHandlers } from "./utils.ts"; import { createHandlers } from "./utils.ts";
import type { BlankEnv, BlankInput } from "hono/types"; import type { BlankEnv, BlankInput } from "hono/types";
import { getVideoSnapshots, getVideoSnapshotsByBV } from "@core/db/videoSnapshot.ts"; import { getVideoSnapshots, getVideoSnapshotsByBV } from "./db/videoSnapshot.ts";
import type { VideoSnapshotType } from "@core/db/schema.d.ts"; import type { VideoSnapshotType } from "@core/db/schema.d.ts";
import { boolean, mixed, number, object, ValidationError } from "yup"; import { boolean, mixed, number, object, ValidationError } from "yup";
import { ErrorResponse } from "./schema";
const SnapshotQueryParamsSchema = object({ const SnapshotQueryParamsSchema = object({
ps: number().integer().optional().positive(), ps: number().integer().optional().positive(),
pn: number().integer().optional().positive(), pn: number().integer().optional().positive(),
offset: number().integer().optional().positive(), offset: number().integer().optional().positive(),
reverse: boolean().optional(), reverse: boolean().optional()
}); });
export const idSchema = mixed().test( export const idSchema = mixed().test(
"is-valid-id", "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', 'id must be a string starting with "av" followed by digits, or "BV" followed by 10 alphanumeric characters, or a positive integer',
async (value) => { async (value) => {
if (value && await number().integer().isValid(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; return Number.isInteger(v) && v > 0;
} }
@ -34,13 +35,11 @@ export const idSchema = mixed().test(
} }
return false; return false;
}, }
); );
type ContextType = Context<BlankEnv, "/video/:id/snapshots", BlankInput>; type ContextType = Context<BlankEnv, "/video/:id/snapshots", BlankInput>;
export const getSnapshotsHanlder = createHandlers(async (c: ContextType) => { export const getSnapshotsHanlder = createHandlers(async (c: ContextType) => {
const client = c.get("db");
try { try {
const idParam = await idSchema.validate(c.req.param("id")); const idParam = await idSchema.validate(c.req.param("id"));
let videoId: string | number = idParam as string; let videoId: string | number = idParam as string;
@ -71,22 +70,32 @@ export const getSnapshotsHanlder = createHandlers(async (c: ContextType) => {
let result: VideoSnapshotType[]; let result: VideoSnapshotType[];
if (typeof videoId === "number") { if (typeof videoId === "number") {
result = await getVideoSnapshots(client, videoId, limit, pageOrOffset, reverse, mode); result = await getVideoSnapshots(videoId, limit, pageOrOffset, reverse, mode);
} else { } else {
result = await getVideoSnapshotsByBV(client, videoId, limit, pageOrOffset, reverse, mode); result = await getVideoSnapshotsByBV(videoId, limit, pageOrOffset, reverse, mode);
} }
const rows = result.map((row) => ({ const rows = result.map((row) => ({
...row, ...row,
aid: Number(row.aid), aid: Number(row.aid)
})); }));
return c.json(rows); return c.json(rows);
} catch (e) { } catch (e: unknown) {
if (e instanceof ValidationError) { if (e instanceof ValidationError) {
return c.json({ message: "Invalid query parameters", errors: e.errors }, 400); const response: ErrorResponse<string> = {
code: "INVALID_QUERY_PARAMS",
message: "Invalid query parameters",
errors: e.errors,
}
return c.json<ErrorResponse<string>>(response, 400);
} else { } else {
return c.json({ message: "Unhandled error", error: e }, 500); const response: ErrorResponse<unknown> = {
code: "UNKNOWN_ERR",
message: "Unhandled error",
errors: [e]
}
return c.json<ErrorResponse<unknown>>(response, 500);
} }
} }
}); });

View File

@ -0,0 +1,19 @@
{
"include": ["**/*.ts"],
"compilerOptions": {
"baseUrl": ".",
"target": "ESNext",
"module": "ESNext",
"useDefineForClassFields": true,
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"paths": {
"@core/*": ["../core/*"],
"@crawler/*": ["../crawler/*"]
},
"allowSyntheticDefaultImports": true,
"allowImportingTsExtensions": true,
"noEmit": true
}
}

View File

@ -1,12 +1,12 @@
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
import { Redis } from "ioredis"; import { Redis } from "ioredis";
import { sql } from "db/db.ts";
import { number, ValidationError } from "yup"; import { number, ValidationError } from "yup";
import { createHandlers } from "./utils.ts"; import { createHandlers } from "./utils.ts";
import { getVideoInfo, getVideoInfoByBV } from "@crawler/net/videoInfo"; import { getVideoInfo, getVideoInfoByBV } from "@core/net/getVideoInfo";
import { idSchema } from "./snapshots.ts"; import { idSchema } from "./snapshots.ts";
import { NetSchedulerError } from "@core/net/delegate.ts"; import { NetSchedulerError } from "@core/net/delegate.ts";
import type { Context } from "hono"; import type { Context } from "hono";
import type { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
import type { BlankEnv, BlankInput } from "hono/types"; import type { BlankEnv, BlankInput } from "hono/types";
import type { VideoInfoData } from "@core/net/bilibili.d.ts"; import type { VideoInfoData } from "@core/net/bilibili.d.ts";
@ -15,7 +15,7 @@ const CACHE_EXPIRATION_SECONDS = 60;
type ContextType = Context<BlankEnv, "/video/:id/info", BlankInput>; type ContextType = Context<BlankEnv, "/video/:id/info", BlankInput>;
async function insertVideoSnapshot(client: Client, data: VideoInfoData) { async function insertVideoSnapshot(data: VideoInfoData) {
const views = data.stat.view; const views = data.stat.view;
const danmakus = data.stat.danmaku; const danmakus = data.stat.danmaku;
const replies = data.stat.reply; const replies = data.stat.reply;
@ -25,16 +25,11 @@ async function insertVideoSnapshot(client: Client, data: VideoInfoData) {
const favorites = data.stat.favorite; const favorites = data.stat.favorite;
const aid = data.aid; const aid = data.aid;
const query: string = ` await sql`
INSERT INTO video_snapshot (aid, views, danmakus, replies, likes, coins, shares, favorites) INSERT INTO video_snapshot (aid, views, danmakus, replies, likes, coins, shares, favorites)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) VALUES (${aid}, ${views}, ${danmakus}, ${replies}, ${likes}, ${coins}, ${shares}, ${favorites})
`; `;
await client.queryObject(
query,
[aid, views, danmakus, replies, likes, coins, shares, favorites],
);
logger.log(`Inserted into snapshot for video ${aid} by videoInfo API.`, "api", "fn:insertVideoSnapshot"); logger.log(`Inserted into snapshot for video ${aid} by videoInfo API.`, "api", "fn:insertVideoSnapshot");
} }

75
packages/core/bun.lock Normal file
View File

@ -0,0 +1,75 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": {
"chalk": "^5.4.1",
"logform": "^2.7.0",
"winston": "^3.17.0",
},
},
},
"packages": {
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="],
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
"color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="],
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
"color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
"colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="],
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
"logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="],
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
}
}

View File

@ -0,0 +1,5 @@
export const SECOND = 1000;
export const MINUTE = 60 * SECOND;
export const HOUR = 60 * MINUTE;
export const DAY = 24 * HOUR;
export const WEEK = 7 * DAY;

View File

@ -21,6 +21,14 @@ export const postgresConfig = {
password: databasePassword, password: databasePassword,
}; };
export const postgresConfigNpm = {
host: databaseHost,
port: parseInt(databasePort),
database: databaseName,
username: databaseUser,
password: databasePassword,
}
export const postgresConfigCred = { export const postgresConfigCred = {
hostname: databaseHost, hostname: databaseHost,
port: parseInt(databasePort), port: parseInt(databasePort),

View File

@ -7,6 +7,9 @@
"db/": "./db/", "db/": "./db/",
"$std/": "https://deno.land/std@0.216.0/", "$std/": "https://deno.land/std@0.216.0/",
"mq/": "./mq/", "mq/": "./mq/",
"chalk": "npm:chalk" "chalk": "npm:chalk",
"winston": "npm:winston",
"logform": "npm:logform",
"@core/": "./"
} }
} }

View File

@ -1,5 +1,5 @@
import winston, { format, transports } from "npm:winston"; import winston, { format, transports } from "winston";
import type { TransformableInfo } from "npm:logform"; import type { TransformableInfo } from "logform";
import chalk from "chalk"; import chalk from "chalk";
const customFormat = format.printf((info: TransformableInfo) => { const customFormat = format.printf((info: TransformableInfo) => {
@ -52,9 +52,9 @@ const createTransport = (level: string, filename: string) => {
}); });
}; };
const sillyLogPath = Deno.env.get("LOG_VERBOSE") ?? "logs/verbose.log"; const sillyLogPath = process.env["LOG_VERBOSE"] ?? "logs/verbose.log";
const warnLogPath = Deno.env.get("LOG_WARN") ?? "logs/warn.log"; const warnLogPath = process.env["LOG_WARN"] ?? "logs/warn.log";
const errorLogPath = Deno.env.get("LOG_ERROR") ?? "logs/error.log"; const errorLogPath = process.env["LOG_ERROR"] ?? "logs/error.log";
const winstonLogger = winston.createLogger({ const winstonLogger = winston.createLogger({
levels: winston.config.npm.levels, levels: winston.config.npm.levels,

View File

@ -1,4 +1,4 @@
import { SlidingWindow } from "./slidingWindow.ts"; import type { SlidingWindow } from "./slidingWindow.ts";
export interface RateLimiterConfig { export interface RateLimiterConfig {
window: SlidingWindow; window: SlidingWindow;

View File

@ -1,4 +1,4 @@
import { Redis } from "ioredis"; import type { Redis } from "ioredis";
export class SlidingWindow { export class SlidingWindow {
private redis: Redis; private redis: Redis;

View File

@ -1,6 +1,6 @@
import networkDelegate from "@core/net/delegate.ts"; import networkDelegate from "@core/net/delegate.ts";
import { VideoInfoData, VideoInfoResponse } from "@core/net/bilibili.d.ts"; import type { VideoInfoData, VideoInfoResponse } from "@core/net/bilibili.d.ts";
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
/* /*
* Fetch video metadata from bilibili API * Fetch video metadata from bilibili API

View File

@ -0,0 +1,7 @@
{
"dependencies": {
"chalk": "^5.4.1",
"logform": "^2.7.0",
"winston": "^3.17.0"
}
}

View File

@ -0,0 +1,18 @@
{
"include": ["**/*.ts"],
"compilerOptions": {
"baseUrl": ".",
"target": "ESNext",
"module": "ESNext",
"useDefineForClassFields": true,
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"paths": {
"@core/*": ["../core/*"]
},
"allowSyntheticDefaultImports": true,
"allowImportingTsExtensions": true,
"noEmit": true
}
}

View File

@ -1,5 +1,5 @@
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts"; import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
import { getVideoInfo } from "net/getVideoInfo.ts"; import { getVideoInfo } from "@core/net/getVideoInfo.ts";
import logger from "log/logger.ts"; import logger from "log/logger.ts";
export interface SnapshotNumber { export interface SnapshotNumber {

View File

@ -52,7 +52,8 @@ async function getVideoSnapshots(aid: number) {
} }
async function getAidFromBV(bv: string) { async function getAidFromBV(bv: string) {
const res = await client.query("SELECT aid FROM bilibili_metadata WHERE bvid = $1", [bv]); const res = await client.query("SELECT aid FROM bilibili_metadata WHERE bvid = $1" +
"", [bv]);
if (res.rows.length <= 0) { if (res.rows.length <= 0) {
return null; return null;
} }