add: complete docker config

This commit is contained in:
alikia2x (寒寒) 2025-04-29 06:12:44 +08:00
parent 784939074a
commit 43b52dee0b
Signed by: alikia2x
GPG Key ID: 56209E0CCD8420C6
22 changed files with 277 additions and 169 deletions

5
.gitignore vendored
View File

@ -91,5 +91,6 @@ model/
*.db *.db
*.sqlite *.sqlite
*.sqlite3 *.sqlite3
db/ data/
data/
docker-compose.yml

30
Dockerfile.frontend Normal file
View File

@ -0,0 +1,30 @@
FROM oven/bun AS bun-builder
WORKDIR /app
COPY ./packages/core ./core
COPY ./packages/frontend/package.json ./packages/frontend/bun.lock ./packages/frontend/tsconfig.json ./packages/frontend/astro.config.mjs ./frontend/
WORKDIR frontend
RUN bun install
COPY ./packages/frontend/ .
RUN bun run build
FROM node:lts-alpine
WORKDIR /app/frontend
COPY --from=bun-builder /app/frontend/dist ./dist
COPY --from=bun-builder /app/frontend/node_modules ./node_modules
COPY --from=bun-builder /app/frontend/package.json ./package.json
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD ["node", "dist/server/entry.mjs"]

10
package.json Normal file
View File

@ -0,0 +1,10 @@
{
"name": "cvsa",
"version": "2.13.22",
"private": false,
"type": "module",
"workspaces": [
"packages/frontend",
"packages/core"
]
}

View File

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

View File

@ -1,4 +1,4 @@
import sql from "./db"; import { sql } from "./db";
import type { VideoSnapshotType } from "@core/db/schema.d.ts"; import type { VideoSnapshotType } from "@core/db/schema.d.ts";
export async function getVideoSnapshots( export async function getVideoSnapshots(

View File

@ -3,7 +3,7 @@ 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 sql from "./db/db.ts"; import { sql } from "./db/db.ts";
import { ErrorResponse, StatusResponse } from "./schema"; import { ErrorResponse, StatusResponse } from "./schema";
const RegistrationBodySchema = object({ const RegistrationBodySchema = object({

View File

@ -34,7 +34,6 @@ async function insertVideoSnapshot(data: VideoInfoData) {
} }
export const videoInfoHandler = createHandlers(async (c: ContextType) => { export const videoInfoHandler = createHandlers(async (c: ContextType) => {
const client = c.get("db");
try { try {
const id = await idSchema.validate(c.req.param("id")); const id = await idSchema.validate(c.req.param("id"));
let videoId: string | number = id as string; let videoId: string | number = id as string;
@ -64,7 +63,7 @@ export const videoInfoHandler = createHandlers(async (c: ContextType) => {
await redis.setex(cacheKey, CACHE_EXPIRATION_SECONDS, JSON.stringify(result)); await redis.setex(cacheKey, CACHE_EXPIRATION_SECONDS, JSON.stringify(result));
await insertVideoSnapshot(client, result); await insertVideoSnapshot(result);
return c.json(result); return c.json(result);
} catch (e) { } catch (e) {

View File

@ -4,9 +4,13 @@
"": { "": {
"dependencies": { "dependencies": {
"chalk": "^5.4.1", "chalk": "^5.4.1",
"ioredis": "^5.6.1",
"logform": "^2.7.0", "logform": "^2.7.0",
"winston": "^3.17.0", "winston": "^3.17.0",
}, },
"devDependencies": {
"@types/ioredis": "^5.0.0",
},
}, },
}, },
"packages": { "packages": {
@ -14,12 +18,18 @@
"@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=="], "@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=="],
"@ioredis/commands": ["@ioredis/commands@1.2.0", "", {}, "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="],
"@types/ioredis": ["@types/ioredis@5.0.0", "", { "dependencies": { "ioredis": "*" } }, "sha512-zJbJ3FVE17CNl5KXzdeSPtdltc4tMT3TzC6fxQS0sQngkbFZ6h+0uTafsRqu+eSLIugf6Yb0Ea0SUuRr42Nk9g=="],
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
"color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], "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-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
@ -30,6 +40,10 @@
"colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="],
"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=="],
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
@ -38,12 +52,18 @@
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"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=="],
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
"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=="], "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=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@ -52,6 +72,10 @@
"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=="], "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=="],
"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=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "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=="], "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
@ -60,6 +84,8 @@
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "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=="], "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],

View File

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

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

@ -1,15 +1,17 @@
{ {
"name": "@cvsa/core", "name": "@cvsa/core",
"exports": "./main.ts", "exports": "./main.ts",
"imports": { "imports": {
"ioredis": "npm:ioredis", "ioredis": "npm:ioredis",
"log/": "./log/", "log/": "./log/",
"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", "winston": "npm:winston",
"logform": "npm:logform", "logform": "npm:logform",
"@core/": "./" "@core/": "./",
} "child_process": "node:child_process",
"util": "node:util"
}
} }

View File

@ -2,8 +2,12 @@ import logger from "log/logger.ts";
import { RateLimiter, type RateLimiterConfig } from "mq/rateLimiter.ts"; import { RateLimiter, type RateLimiterConfig } from "mq/rateLimiter.ts";
import { SlidingWindow } from "mq/slidingWindow.ts"; import { SlidingWindow } from "mq/slidingWindow.ts";
import { redis } from "db/redis.ts"; import { redis } from "db/redis.ts";
import Redis from "ioredis"; import { ReplyError } from "ioredis";
import { SECOND } from "$std/datetime/constants.ts"; import { SECOND } from "../const/time.ts";
import { execFile } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(execFile);
interface Proxy { interface Proxy {
type: string; type: string;
@ -99,7 +103,7 @@ class NetworkDelegate {
await this.providerLimiters[providerLimiterId]?.trigger(); await this.providerLimiters[providerLimiterId]?.trigger();
} catch (e) { } catch (e) {
const error = e as Error; const error = e as Error;
if (e instanceof Redis.ReplyError) { if (e instanceof ReplyError) {
logger.error(error, "redis"); logger.error(error, "redis");
} }
logger.warn(`Unhandled error: ${error.message}`, "mq", "proxyRequest"); logger.warn(`Unhandled error: ${error.message}`, "mq", "proxyRequest");
@ -209,7 +213,7 @@ class NetworkDelegate {
return providerAvailable && proxyAvailable; return providerAvailable && proxyAvailable;
} catch (e) { } catch (e) {
const error = e as Error; const error = e as Error;
if (e instanceof Redis.ReplyError) { if (e instanceof ReplyError) {
logger.error(error, "redis"); logger.error(error, "redis");
return false; return false;
} }
@ -239,8 +243,7 @@ class NetworkDelegate {
private async alicloudFcRequest<R>(url: string, region: string): Promise<R> { private async alicloudFcRequest<R>(url: string, region: string): Promise<R> {
try { try {
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const output = await new Deno.Command("aliyun", { const output = await execAsync("aliyun", [
args: [
"fc", "fc",
"POST", "POST",
`/2023-03-30/functions/proxy-${region}/invocations`, `/2023-03-30/functions/proxy-${region}/invocations`,
@ -258,9 +261,8 @@ class NetworkDelegate {
"10", "10",
"--profile", "--profile",
`CVSA-${region}`, `CVSA-${region}`,
], ])
}).output(); const out = output.stdout;
const out = decoder.decode(output.stdout);
const rawData = JSON.parse(out); const rawData = JSON.parse(out);
if (rawData.statusCode !== 200) { if (rawData.statusCode !== 200) {
// noinspection ExceptionCaughtLocallyJS // noinspection ExceptionCaughtLocallyJS

View File

@ -1,7 +1,11 @@
{ {
"dependencies": { "dependencies": {
"chalk": "^5.4.1", "chalk": "^5.4.1",
"ioredis": "^5.6.1",
"logform": "^2.7.0", "logform": "^2.7.0",
"winston": "^3.17.0" "winston": "^3.17.0"
},
"devDependencies": {
"@types/ioredis": "^5.0.0"
} }
} }

View File

@ -9,7 +9,7 @@
"lib": ["ES2020", "DOM", "DOM.Iterable"], "lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true, "skipLibCheck": true,
"paths": { "paths": {
"@core/*": ["../core/*"] "@core/*": ["./*"]
}, },
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,

View File

@ -1,27 +0,0 @@
FROM oven/bun AS bun-builder
WORKDIR /app
COPY package.json bun.lock tsconfig.json astro.config.mjs ./
RUN bun install
COPY . .
RUN bun run build
FROM node:lts-alpine
WORKDIR /app
COPY --from=bun-builder /app/dist ./dist
COPY --from=bun-builder /app/node_modules ./node_modules
COPY --from=bun-builder /app/package.json ./package.json
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD ["node", "dist/server/entry.mjs"]

View File

@ -0,0 +1,8 @@
import { sql } from "@core/db/dbNew";
export async function aidExists(aid: number) {
const res = await sql`
SELECT 1 FROM bilibili_metadata WHERE aid = ${aid}
`;
return res.length > 0;
}

View File

@ -0,0 +1,15 @@
import { sql } from "@core/db/dbNew";
export async function getAidFromBV(bv: string) {
const res = await sql`
SELECT aid FROM bilibili_metadata WHERE bvid = ${bv}
`
if (res.length <= 0) {
return null;
}
const row = res[0];
if (row && row.aid) {
return Number(row.aid);
}
return null;
}

View File

@ -0,0 +1,15 @@
import { sql } from "@core/db/dbNew";
export async function getVideoMetadata(aid: number) {
const res = await sql`
SELECT * FROM bilibili_metadata WHERE aid = ${aid}
`;
if (res.length <= 0) {
return null;
}
const row = res[0];
if (row) {
return row;
}
return {};
}

View File

@ -0,0 +1,11 @@
import { sql } from "@core/db/dbNew";
export async function getAllSnapshots(aid: number) {
const res = await sql`
SELECT * FROM video_snapshot WHERE aid = ${aid} ORDER BY created_at DESC
`;
if (res.length <= 0) {
return null;
}
return res;
}

View File

@ -1,73 +1,31 @@
--- ---
import Layout from "@layouts/Layout.astro"; import Layout from "@layouts/Layout.astro";
import TitleBar from "@components/TitleBar.astro"; import TitleBar from "@components/TitleBar.astro";
import pg from "pg"; import { format } from "date-fns";
import { format } from 'date-fns'; import { zhCN } from "date-fns/locale";
import { zhCN } from 'date-fns/locale';
import MetadataRow from "@components/InfoPage/MetadataRow.astro"; import MetadataRow from "@components/InfoPage/MetadataRow.astro";
import { getAllSnapshots } from "src/db/snapshots/getAllSnapshots";
import { getAidFromBV } from "src/db/bilibili_metadata/getAidFromBV";
import { getVideoMetadata } from "src/db/bilibili_metadata/getVideoMetadata";
import { aidExists as idExists } from "src/db/bilibili_metadata/aidExists";
const databaseHost = import.meta.env.DB_HOST const databaseHost = import.meta.env.DB_HOST;
const databaseName = import.meta.env.DB_NAME const databaseName = import.meta.env.DB_NAME;
const databaseUser = import.meta.env.DB_USER const databaseUser = import.meta.env.DB_USER;
const databasePassword = import.meta.env.DB_PASSWORD const databasePassword = import.meta.env.DB_PASSWORD;
const databasePort = import.meta.env.DB_PORT const databasePort = import.meta.env.DB_PORT;
const postgresConfig = { const postgresConfig = {
hostname: databaseHost, hostname: databaseHost,
port: parseInt(databasePort!), port: parseInt(databasePort!),
database: databaseName, database: databaseName,
user: databaseUser, user: databaseUser,
password: databasePassword, password: databasePassword,
}; };
console.log(postgresConfig) console.log(postgresConfig);
// 路由参数
const { id } = Astro.params; const { id } = Astro.params;
const { Client } = pg;
const client = new Client(postgresConfig);
await client.connect();
// 数据库查询函数
async function getVideoMetadata(aid: number) {
const res = await client.query("SELECT * FROM bilibili_metadata WHERE aid = $1", [aid]);
if (res.rows.length <= 0) {
return null;
}
const row = res.rows[0];
if (row) {
return row;
}
return {};
}
async function getVideoSnapshots(aid: number) {
const res = await client.query("SELECT * FROM video_snapshot WHERE aid = $1 ORDER BY created_at DESC", [
aid,
]);
if (res.rows.length <= 0) {
return null;
}
return res.rows;
}
async function getAidFromBV(bv: string) {
const res = await client.query("SELECT aid FROM bilibili_metadata WHERE bvid = $1" +
"", [bv]);
if (res.rows.length <= 0) {
return null;
}
const row = res.rows[0];
if (row && row.aid) {
return Number(row.aid);
}
return null;
}
async function idExists(aid: number) {
const res = await client.query("SELECT COUNT(*) FROM bilibili_metadata WHERE aid = $1", [aid]);
return res.rows[0].count > 0;
}
async function getVideoAid(id: string) { async function getVideoAid(id: string) {
if (id.startsWith("av")) { if (id.startsWith("av")) {
@ -78,27 +36,22 @@ async function getVideoAid(id: string) {
return parseInt(id); return parseInt(id);
} }
// 获取数据
if (!id) { if (!id) {
Astro.response.status = 404; Astro.response.status = 404;
client.end();
return new Response(null, { status: 404 }); return new Response(null, { status: 404 });
} }
const aid = await getVideoAid(id); const aid = await getVideoAid(id);
if (!aid || isNaN(aid)) { if (!aid || isNaN(aid)) {
Astro.response.status = 404; Astro.response.status = 404;
client.end();
return new Response(null, { status: 404 }); return new Response(null, { status: 404 });
} }
const aidExists = await idExists(aid); const aidExists = await idExists(aid);
if (!aidExists) { if (!aidExists) {
Astro.response.status = 404; Astro.response.status = 404;
client.end();
return new Response(null, { status: 404 }); return new Response(null, { status: 404 });
} }
const videoInfo = await getVideoMetadata(aid); const videoInfo = await getVideoMetadata(aid);
const snapshots = await getVideoSnapshots(aid); const snapshots = await getAllSnapshots(aid);
client.end();
interface Snapshot { interface Snapshot {
created_at: Date; created_at: Date;
@ -117,24 +70,36 @@ interface Snapshot {
<TitleBar /> <TitleBar />
<main class="flex flex-col items-center min-h-screen gap-8 mt-10 md:mt-6 relative z-0 overflow-x-auto pb-8"> <main class="flex flex-col items-center min-h-screen gap-8 mt-10 md:mt-6 relative z-0 overflow-x-auto pb-8">
<div class="w-full lg:max-w-4xl lg:mx-auto lg:p-6"> <div class="w-full lg:max-w-4xl lg:mx-auto lg:p-6">
<h1 class="text-2xl font-medium ml-2 mb-4">视频信息: <a href={`https://www.bilibili.com/video/av${aid}`} class="underline ">av{aid}</a></h1> <h1 class="text-2xl font-medium ml-2 mb-4">
视频信息: <a href={`https://www.bilibili.com/video/av${aid}`} class="underline">av{aid}</a>
</h1>
<div class="mb-6"> <div class="mb-6">
<h2 class="px-2 mb-2 text-xl font-medium">基本信息</h2> <h2 class="px-2 mb-2 text-xl font-medium">基本信息</h2>
<div class="overflow-x-auto max-w-full px-2"> <div class="overflow-x-auto max-w-full px-2">
<table class="table-fixed"> <table class="table-fixed">
<tbody> <tbody>
<MetadataRow title={id} description={videoInfo?.id}/> <MetadataRow title={id} description={videoInfo?.id} />
<MetadataRow title={videoInfo?.aid} description={videoInfo?.aid}/> <MetadataRow title={videoInfo?.aid} description={videoInfo?.aid} />
<MetadataRow title={videoInfo?.bvid} description={videoInfo?.bvid}/> <MetadataRow title={videoInfo?.bvid} description={videoInfo?.bvid} />
<MetadataRow title="标题" description={videoInfo?.title}/> <MetadataRow title="标题" description={videoInfo?.title} />
<MetadataRow title="描述" description={videoInfo?.description}/> <MetadataRow title="描述" description={videoInfo?.description} />
<MetadataRow title="UID" description={videoInfo?.uid}/> <MetadataRow title="UID" description={videoInfo?.uid} />
<MetadataRow title="标签" description={videoInfo?.tags}/> <MetadataRow title="标签" description={videoInfo?.tags} />
<MetadataRow title="发布时间" description={format(new Date(videoInfo?.published_at), 'yyyy-MM-dd HH:mm:ss', { locale: zhCN })}/> <MetadataRow
<MetadataRow title="时长 (秒)" description={videoInfo?.duration}/> title="发布时间"
<MetadataRow title="创建时间" description={format(new Date(videoInfo?.created_at), 'yyyy-MM-dd HH:mm:ss', { locale: zhCN })}/> description={format(new Date(videoInfo?.published_at), "yyyy-MM-dd HH:mm:ss", {
<MetadataRow title="封面" description={videoInfo?.cover_url}/> locale: zhCN,
})}
/>
<MetadataRow title="时长 (秒)" description={videoInfo?.duration} />
<MetadataRow
title="创建时间"
description={format(new Date(videoInfo?.created_at), "yyyy-MM-dd HH:mm:ss", {
locale: zhCN,
})}
/>
<MetadataRow title="封面" description={videoInfo?.cover_url} />
</tbody> </tbody>
</table> </table>
</div> </div>
@ -142,40 +107,46 @@ interface Snapshot {
<div> <div>
<h2 class="px-2 mb-2 text-xl font-medium">播放量历史数据</h2> <h2 class="px-2 mb-2 text-xl font-medium">播放量历史数据</h2>
{snapshots && snapshots.length > 0 ? ( {
<div class="overflow-x-auto px-2"> snapshots && snapshots.length > 0 ? (
<table class="table-auto w-full"> <div class="overflow-x-auto px-2">
<thead> <table class="table-auto w-full">
<tr> <thead>
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">创建时间</th>
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">观看</th>
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">硬币</th>
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">点赞</th>
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">收藏</th>
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">分享</th>
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">弹幕</th>
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">评论</th>
</tr>
</thead>
<tbody>
{snapshots.map((snapshot: Snapshot) => (
<tr> <tr>
<td class="border dark:border-zinc-500 px-4 py-2">{format(new Date(snapshot.created_at), 'yyyy-MM-dd HH:mm:ss', { locale: zhCN })}</td> <th class="border dark:border-zinc-500 px-4 py-2 font-medium">创建时间</th>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.views}</td> <th class="border dark:border-zinc-500 px-4 py-2 font-medium">观看</th>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.coins}</td> <th class="border dark:border-zinc-500 px-4 py-2 font-medium">硬币</th>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.likes}</td> <th class="border dark:border-zinc-500 px-4 py-2 font-medium">点赞</th>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.favorites}</td> <th class="border dark:border-zinc-500 px-4 py-2 font-medium">收藏</th>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.shares}</td> <th class="border dark:border-zinc-500 px-4 py-2 font-medium">分享</th>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.danmakus}</td> <th class="border dark:border-zinc-500 px-4 py-2 font-medium">弹幕</th>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.replies}</td> <th class="border dark:border-zinc-500 px-4 py-2 font-medium">评论</th>
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table> {snapshots.map((snapshot: Snapshot) => (
</div> <tr>
) : ( <td class="border dark:border-zinc-500 px-4 py-2">
<p>暂无历史数据。</p> {format(new Date(snapshot.created_at), "yyyy-MM-dd HH:mm:ss", {
)} locale: zhCN,
})}
</td>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.views}</td>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.coins}</td>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.likes}</td>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.favorites}</td>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.shares}</td>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.danmakus}</td>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.replies}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p>暂无历史数据。</p>
)
}
</div> </div>
</div> </div>
</main> </main>

View File

@ -11,6 +11,7 @@
"@assets/*": ["src/assets/*"], "@assets/*": ["src/assets/*"],
"@styles": ["src/styles/*"], "@styles": ["src/styles/*"],
"@core/*": ["../core/*"] "@core/*": ["../core/*"]
} },
"verbatimModuleSyntax": true
} }
} }