merge: branch 'main' into feat/backend

This commit is contained in:
alikia2x (寒寒) 2025-05-10 00:19:55 +08:00
commit bf00918c00
Signed by: alikia2x
GPG Key ID: 56209E0CCD8420C6
119 changed files with 4611 additions and 1311 deletions

96
.dockerignore Normal file
View File

@ -0,0 +1,96 @@
built/*
tests/cases/rwc/*
tests/cases/perf/*
!tests/cases/webharness/compilerToString.js
test-args.txt
~*.docx
\#*\#
.\#*
tests/baselines/local/*
tests/baselines/local.old/*
tests/services/baselines/local/*
tests/baselines/prototyping/local/*
tests/baselines/rwc/*
tests/baselines/reference/projectOutput/*
tests/baselines/local/projectOutput/*
tests/baselines/reference/testresults.tap
tests/baselines/symlinks/*
tests/services/baselines/prototyping/local/*
tests/services/browser/typescriptServices.js
src/harness/*.js
src/compiler/diagnosticInformationMap.generated.ts
src/compiler/diagnosticMessages.generated.json
src/parser/diagnosticInformationMap.generated.ts
src/parser/diagnosticMessages.generated.json
rwc-report.html
*.swp
build.json
*.actual
tests/webTestServer.js
tests/webTestServer.js.map
tests/webhost/*.d.ts
tests/webhost/webtsc.js
tests/cases/**/*.js
tests/cases/**/*.js.map
*.config
scripts/eslint/built/
scripts/debug.bat
scripts/run.bat
scripts/**/*.js
scripts/**/*.js.map
coverage/
internal/
**/.DS_Store
.settings
**/.vs
**/.vscode/*
!**/.vscode/tasks.json
!**/.vscode/settings.template.json
!**/.vscode/launch.template.json
!**/.vscode/extensions.json
!tests/cases/projects/projectOption/**/node_modules
!tests/cases/projects/NodeModulesSearch/**/*
!tests/baselines/reference/project/nodeModules*/**/*
yarn.lock
yarn-error.log
.parallelperf.*
tests/baselines/reference/dt
.failed-tests
TEST-results.xml
package-lock.json
.eslintcache
*v8.log
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# npm dependencies
node_modules/
# project specific
logs/
__pycache__
ml/filter/runs
ml/pred/runs
ml/pred/checkpoints
ml/pred/observed
ml/data/
ml/filter/checkpoints
scripts
model/
.astro
# Database
*.dump
*.db
*.sqlite
*.sqlite3
data/
docker-compose.yml

70
.gitignore vendored
View File

@ -1,72 +1,14 @@
built/*
tests/cases/rwc/*
tests/cases/perf/*
!tests/cases/webharness/compilerToString.js
test-args.txt
~*.docx
\#*\#
.\#*
tests/baselines/local/*
tests/baselines/local.old/*
tests/services/baselines/local/*
tests/baselines/prototyping/local/*
tests/baselines/rwc/*
tests/baselines/reference/projectOutput/*
tests/baselines/local/projectOutput/*
tests/baselines/reference/testresults.tap
tests/baselines/symlinks/*
tests/services/baselines/prototyping/local/*
tests/services/browser/typescriptServices.js
src/harness/*.js
src/compiler/diagnosticInformationMap.generated.ts
src/compiler/diagnosticMessages.generated.json
src/parser/diagnosticInformationMap.generated.ts
src/parser/diagnosticMessages.generated.json
rwc-report.html
*.swp
build.json
*.actual
tests/webTestServer.js
tests/webTestServer.js.map
tests/webhost/*.d.ts
tests/webhost/webtsc.js
tests/cases/**/*.js
tests/cases/**/*.js.map
*.config
scripts/eslint/built/
scripts/debug.bat
scripts/run.bat
scripts/**/*.js
scripts/**/*.js.map
coverage/
internal/
**/.DS_Store **/.DS_Store
.settings .settings
**/.vs **/.vs
**/.vscode/* **/.vscode/*
!**/.vscode/tasks.json
!**/.vscode/settings.template.json
!**/.vscode/launch.template.json
!**/.vscode/extensions.json
!tests/cases/projects/projectOption/**/node_modules
!tests/cases/projects/NodeModulesSearch/**/*
!tests/baselines/reference/project/nodeModules*/**/*
yarn.lock
yarn-error.log
.parallelperf.*
tests/baselines/reference/dt
.failed-tests
TEST-results.xml
package-lock.json
.eslintcache .eslintcache
*v8.log *v8.log
# dotenv environment variable files # dotenv environment variable files
.env .env
.env.development.local .env.*
.env.test.local
.env.production.local
.env.local
# npm dependencies # npm dependencies
node_modules/ node_modules/
@ -83,7 +25,6 @@ ml/filter/checkpoints
scripts scripts
model/ model/
.astro .astro
# Database # Database
@ -92,3 +33,10 @@ model/
*.sqlite *.sqlite
*.sqlite3 *.sqlite3
data/ data/
redis/
# Build
dist/
build/
docker-compose.yml

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

@ -26,6 +26,8 @@
<excludeFolder url="file://$MODULE_DIR$/packages/backend/logs" /> <excludeFolder url="file://$MODULE_DIR$/packages/backend/logs" />
<excludeFolder url="file://$MODULE_DIR$/packages/core/net/logs" /> <excludeFolder url="file://$MODULE_DIR$/packages/core/net/logs" />
<excludeFolder url="file://$MODULE_DIR$/packages/crawler/logs" /> <excludeFolder url="file://$MODULE_DIR$/packages/crawler/logs" />
<excludeFolder url="file://$MODULE_DIR$/data" />
<excludeFolder url="file://$MODULE_DIR$/redis" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />

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>

8
.prettierrc Normal file
View File

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

View File

@ -1,6 +0,0 @@
{
"recommendations": [
"denoland.vscode-deno",
"bradlc.vscode-tailwindcss"
]
}

View File

@ -1,35 +0,0 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{
"lsp": {
"deno": {
"settings": {
"deno": {
"enable": true
}
}
}
},
"languages": {
"TypeScript": {
"language_servers": [
"deno",
"!typescript-language-server",
"!vtsls",
"!eslint"
],
"formatter": "language_server"
},
"TSX": {
"language_servers": [
"deno",
"!typescript-language-server",
"!vtsls",
"!eslint"
],
"formatter": "language_server"
}
}
}

17
Dockerfile.backend Normal file
View File

@ -0,0 +1,17 @@
FROM oven/bun
WORKDIR /app
COPY ./packages/core ./core
COPY ./packages/backend/package.json ./packages/backend/bun.lock ./backend/
WORKDIR backend
RUN bun install
COPY ./packages/backend/ .
RUN mkdir -p /app/logs
CMD ["bun", "start"]

19
Dockerfile.crawler Normal file
View File

@ -0,0 +1,19 @@
FROM oven/bun:1.2.8-debian
WORKDIR /app
COPY . .
RUN bun i
RUN mkdir -p /app/logs
RUN apt update && apt install -y curl
RUN ln -s /bin/uname /usr/bin/uname
RUN /bin/bash -c "$(curl -fsSL https://aliyuncli.alicdn.com/install.sh)"
WORKDIR packages/crawler
CMD ["bun", "all"]

20
Dockerfile.frontend Normal file
View File

@ -0,0 +1,20 @@
FROM oven/bun
WORKDIR /app
COPY . .
RUN bun install
WORKDIR packages/frontend
RUN bun run build
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
RUN mkdir -p /app/logs
CMD ["bun", "/app/packages/frontend/dist/server/entry.mjs"]

1620
bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"lock": false, "lock": false,
"workspace": ["./packages/crawler", "./packages/frontend", "./packages/backend", "./packages/core"], "workspace": ["./packages/crawler", "./packages/core"],
"nodeModulesDir": "auto", "nodeModulesDir": "auto",
"tasks": { "tasks": {
"crawler": "deno task --filter 'crawler' all", "crawler": "deno task --filter 'crawler' all",
@ -12,10 +12,5 @@
"indentWidth": 4, "indentWidth": 4,
"semiColons": true, "semiColons": true,
"proseWrap": "always" "proseWrap": "always"
},
"imports": {
"@astrojs/node": "npm:@astrojs/node@^9.1.3",
"@astrojs/svelte": "npm:@astrojs/svelte@^7.0.8",
"date-fns": "npm:date-fns@^4.1.0"
} }
} }

View File

@ -0,0 +1,71 @@
version: '3.8'
services:
db:
image: postgres:17
ports:
- "5431:5432"
environment:
POSTGRES_USER: cvsa
POSTGRES_PASSWORD: ""
POSTGRES_DB: cvsa_main
volumes:
- ./data:/var/lib/postgresql/data
redis:
image: redis:latest
ports:
- "6378:6379"
volumes:
- ./redis/data:/data
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf
- ./redis/logs:/logs
frontend:
build:
context: .
dockerfile: Dockerfile.frontend
ports:
- "4321:4321"
environment:
- HOST=0.0.0.0
- PORT=4321
- DB_HOST=db
- DB_NAME=cvsa_main
- DB_NAME_CRED=cvsa_cred
- DB_USER=cvsa
- DB_PORT=5432
- DB_PASSWORD=""
- LOG_VERBOSE=/app/logs/verbose.log
- LOG_WARN=/app/logs/warn.log
- LOG_ERR=/app/logs/error.log
depends_on:
- db
volumes:
- /path/to/your/logs:/app/logs
backend:
build:
context: .
dockerfile: Dockerfile.backend
ports:
- "8000:8000"
environment:
- HOST=0.0.0.0
- DB_HOST=db
- DB_NAME=cvsa_main
- DB_NAME_CRED=cvsa_cred
- DB_USER=cvsa
- DB_PORT=5432
- DB_PASSWORD=""
- LOG_VERBOSE=/app/logs/verbose.log
- LOG_WARN=/app/logs/warn.log
- LOG_ERR=/app/logs/error.log
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
- db
volumes:
- /path/to/your/logs:/app/logs
volumes:
db_data:

20
package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "cvsa",
"version": "2.13.22",
"private": false,
"type": "module",
"workspaces": [
"packages/frontend",
"packages/core",
"packages/backend",
"packages/crawler"
],
"dependencies": {
"postgres": "^3.4.5"
},
"devDependencies": {
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.2",
"vitest-tsconfig-paths": "^3.4.1"
}
}

View File

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

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

@ -0,0 +1,208 @@
{
"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",
"rate-limit-redis": "^4.2.0",
"yup": "^1.6.1",
"zod": "^3.24.3",
},
"devDependencies": {
"@types/bun": "^1.2.11",
"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=="],
"@types/bun": ["@types/bun@1.2.11", "", { "dependencies": { "bun-types": "1.2.11" } }, "sha512-ZLbbI91EmmGwlWTRWuV6J19IUiUC5YQ3TCEuSHI3usIP75kuoA8/0PVF+LTrbEnVc8JIhpElWOxv1ocI1fJBbw=="],
"@types/node": ["@types/node@22.15.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
"bun-types": ["bun-types@1.2.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-dbkp5Lo8HDrXkLrONm6bk+yiiYQSntvFUzQp0v3pzTAsXk6FtgVMjdQ+lzFNVAmQFUkPQZ3WMZqH5tTo+Dp/IA=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"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=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="],
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"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=="],
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"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=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
"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=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"rate-limit-redis": ["rate-limit-redis@4.2.0", "", { "peerDependencies": { "express-rate-limit": ">= 6" } }, "sha512-wV450NQyKC24NmPosJb2131RoczLdfIJdKCReNwtVpm5998U8SgKrAZrIHaN/NfQgqOHaan8Uq++B4sa5REwjA=="],
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
"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=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
"tiny-case": ["tiny-case@1.0.3", "", {}, "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"toposort": ["toposort@2.0.2", "", {}, "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="],
"type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"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,46 @@
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 postgresCredConfigNpm = {
host: databaseHost,
port: parseInt(databasePort),
database: databaseNameCred,
username: databaseUser,
password: databasePassword
}
export const postgresConfigCred = {
hostname: databaseHost,
port: parseInt(databasePort),
database: databaseNameCred,
user: databaseUser,
password: databasePassword
};

View File

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

View File

@ -1,21 +1,21 @@
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
import { Redis } from "ioredis"; import { redis } from "@core/db/redis.ts";
import { sql } from "./db.ts";
import { number, ValidationError } from "yup"; import { number, ValidationError } from "yup";
import { createHandlers } from "./utils.ts"; import { createHandlers } from "../src/utils.ts";
import { getVideoInfo, getVideoInfoByBV } from "@crawler/net/videoInfo"; import { getVideoInfo, getVideoInfoByBV } from "@core/net/getVideoInfo.ts";
import { idSchema } from "./snapshots.ts"; import { idSchema } from "../routes/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";
import { startTime, endTime } from 'hono/timing'
const redis = new Redis({ maxRetriesPerRequest: null });
const CACHE_EXPIRATION_SECONDS = 60; 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,21 +25,16 @@ 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");
} }
export const videoInfoHandler = createHandlers(async (c: ContextType) => { export const videoInfoHandler = createHandlers(async (c: ContextType) => {
const client = c.get("db"); startTime(c, 'parse', 'Parse the request');
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;
@ -50,27 +45,33 @@ export const videoInfoHandler = createHandlers(async (c: ContextType) => {
} }
const cacheKey = `cvsa:videoInfo:${videoId}`; const cacheKey = `cvsa:videoInfo:${videoId}`;
endTime(c, 'parse');
startTime(c, 'cache', 'Check for cached data');
const cachedData = await redis.get(cacheKey); const cachedData = await redis.get(cacheKey);
endTime(c, 'cache');
if (cachedData) { if (cachedData) {
return c.json(JSON.parse(cachedData)); return c.json(JSON.parse(cachedData));
} }
startTime(c, 'net', 'Fetch data');
let result: VideoInfoData | number; let result: VideoInfoData | number;
if (typeof videoId === "number") { if (typeof videoId === "number") {
result = await getVideoInfo(videoId, "getVideoInfo"); result = await getVideoInfo(videoId, "getVideoInfo");
} else { } else {
result = await getVideoInfoByBV(videoId, "getVideoInfo"); result = await getVideoInfoByBV(videoId, "getVideoInfo");
} }
endTime(c, 'net');
if (typeof result === "number") { if (typeof result === "number") {
return c.json({ message: "Error fetching video info", code: result }, 500); return c.json({ message: "Error fetching video info", code: result }, 500);
} }
startTime(c, 'db', 'Write data to database');
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);
endTime(c, 'db');
return c.json(result); return c.json(result);
} catch (e) { } catch (e) {
if (e instanceof ValidationError) { if (e instanceof ValidationError) {

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 +0,0 @@
import { Hono } from "hono";
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.get("/", ...rootHandler);
app.get("/video/:id/snapshots", ...getSnapshotsHanlder);
app.post("/user", ...registerHandler);
app.get("/video/:id/info", ...videoInfoHandler);
const fetch = app.fetch;
export default {
fetch,
} satisfies Deno.ServeDefaultExport;
export const VERSION = "0.4.2";

View File

@ -0,0 +1,14 @@
import { bodyLimit } from "hono/body-limit";
import { ErrorResponse } from "../src/schema";
export const bodyLimitForPing = bodyLimit({
maxSize: 14000,
onError: (c) => {
const res: ErrorResponse<string> = {
message: "Body too large",
errors: ["Body should not be larger than 14kB."],
code: "BODY_TOO_LARGE"
};
return c.json(res, 413);
}
});

View File

@ -0,0 +1,6 @@
import { Context, Next } from "hono";
export const contentType = async (c: Context, next: Next) => {
await next();
c.header("Content-Type", "application/json; charset=utf-8");
};

View File

@ -0,0 +1,19 @@
import { Hono } from "hono";
import { Variables } from "hono/types";
import { bodyLimitForPing } from "./bodyLimits.ts";
import { pingHandler } from "../routes/ping.ts";
import { registerRateLimiter } from "./rateLimiters.ts";
import { preetifyResponse } from "./preetifyResponse.ts";
import { logger } from "./logger.ts";
import { timing } from "hono/timing";
import { contentType } from "./contentType.ts";
export function configureMiddleWares(app: Hono<{Variables: Variables }>) {
app.use("*", contentType);
app.use(timing());
app.use("*", preetifyResponse);
app.use("*", logger({}));
app.post("/user", registerRateLimiter);
app.all("/ping", bodyLimitForPing, ...pingHandler);
}

View File

@ -0,0 +1,160 @@
// Color constants
import { Context, Next } from "hono";
import { TimingVariables } from "hono/timing";
import { getConnInfo } from "hono/bun";
const green = "\x1b[97;42m";
const white = "\x1b[90;47m";
const yellow = "\x1b[90;43m";
const red = "\x1b[97;41m";
const blue = "\x1b[97;44m";
const magenta = "\x1b[97;45m";
const cyan = "\x1b[97;46m";
const reset = "\x1b[0m";
let consoleColorMode = "auto";
function formatCurrentTime() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0"); // Month is 0-indexed
const day = String(now.getDate()).padStart(2, "0");
const hours = String(now.getHours()).padStart(2, "0");
const minutes = String(now.getMinutes()).padStart(2, "0");
const seconds = String(now.getSeconds()).padStart(2, "0");
const milliseconds = String(now.getMilliseconds()).padStart(3, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
}
export const DisableConsoleColor = () => {
consoleColorMode = "disable";
};
export const ForceConsoleColor = () => {
consoleColorMode = "force";
};
const defaultFormatter = (params) => {
const latency = params.latency > 60000 ? `${Math.round(params.latency / 1000)}s` : `${params.latency}ms`;
let statusColor = white;
if (params.isOutputColor) {
if (params.status >= 100 && params.status < 300) statusColor = green;
else if (params.status >= 300 && params.status < 400) statusColor = white;
else if (params.status >= 400 && params.status < 500) statusColor = yellow;
else statusColor = red;
}
let methodColor = reset;
switch (params.method) {
case "GET":
methodColor = blue;
break;
case "POST":
methodColor = cyan;
break;
case "PUT":
methodColor = yellow;
break;
case "DELETE":
methodColor = red;
break;
case "PATCH":
methodColor = green;
break;
case "HEAD":
methodColor = magenta;
break;
case "OPTIONS":
methodColor = white;
break;
}
return (
`${params.timestamp} |${statusColor} ${params.status} ${reset}| ` +
`${latency.padStart(7)} | ${params.ip.padStart(16)} |` +
`${methodColor} ${params.method.padEnd(6)}${reset} ${params.path}`
);
};
type Ctx = Context
export const logger = (config) => {
const { formatter = defaultFormatter, output = console, skipPaths = [], skip = null } = config;
// Convert skipPaths to Set for faster lookups
const skipPathsSet = new Set(skipPaths);
return async (c: Ctx, next: Next) => {
const start = Date.now();
const url = new URL(c.req.url);
const path = url.pathname;
// Check if we should skip logging
if (skipPathsSet.has(path) || (typeof skip === "function" && skip(c))) {
return next();
}
try {
await next();
} catch (error) {
// Handle errors
const errorParams = {
timestamp: formatCurrentTime(),
latency: Date.now() - start,
status: 500,
ip: getClientIP(c),
method: c.req.method,
path,
error: error.message,
isOutputColor: shouldColorize(c)
};
output.error(
formatter({
...errorParams,
errorMessage: error.message
})
);
throw error;
}
const status = c.res.status;
const latency = Date.now() - start;
const params = {
timestamp: formatCurrentTime(),
latency,
status,
ip: getClientIP(c),
method: c.req.method,
path,
bodySize: c.res.headers.get("content-length") || 0,
isOutputColor: shouldColorize(c)
};
// Format and output the log
const logMessage = formatter(params);
if (status >= 400 && status < 500) {
output.warn?.(logMessage) || output.log(logMessage);
} else if (status >= 500) {
output.error?.(logMessage) || output.log(logMessage);
} else {
output.log(logMessage);
}
};
};
function shouldColorize(c) {
if (consoleColorMode === "disable") return false;
if (consoleColorMode === "force") return true;
// In development environment with TTY
return process.stdout.isTTY;
}
export function getClientIP(c: Ctx) {
const info = getConnInfo(c);
return info.remote.address;
}

View File

@ -0,0 +1,18 @@
import { startTime, endTime } from "hono/timing";
import { Context, Next } from "hono";
export const preetifyResponse = async (c: Context, next: Next) => {
await next();
const contentType = c.res.headers.get("Content-Type") || "";
if (!contentType.includes("application/json")) return;
const accept = c.req.header("Accept") || "";
const secFetchMode = c.req.header("Sec-Fetch-Mode");
const isBrowser = accept.includes("text/html") || secFetchMode === "navigate";
if (isBrowser) {
const json = await c.res.json();
startTime(c, "seralize", "Prettify the response");
const prettyJson = JSON.stringify(json, null, 2);
endTime(c, "seralize");
c.res = new Response(prettyJson, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
}
};

View File

@ -0,0 +1,27 @@
import { rateLimiter, Store } from "hono-rate-limiter";
import type { BlankEnv } from "hono/types";
import { MINUTE } from "@core/const/time.ts";
import { getConnInfo } from "hono/bun";
import type { Context } from "hono";
import { redis } from "@core/db/redis.ts";
import { RedisStore } from "rate-limit-redis";
export const registerRateLimiter = rateLimiter<BlankEnv, "/user", {}>({
windowMs: 60 * MINUTE,
limit: 10,
standardHeaders: "draft-6",
keyGenerator: (c) => {
const info = getConnInfo(c as unknown as Context<BlankEnv, "/user", {}>);
if (!info.remote || !info.remote.address) {
return crypto.randomUUID();
}
const addr = info.remote.address;
const path = new URL(c.req.url).pathname;
const method = c.req.method;
return `${method}-${path}@${addr}`;
},
store: new RedisStore({
// @ts-expect-error - Known issue: the `c`all` function is not present in @types/ioredis
sendCommand: (...args: string[]) => redis.call(...args)
}) as unknown as Store
});

View File

@ -0,0 +1,22 @@
{
"name": "backend",
"scripts": {
"format": "prettier --write .",
"dev": "NODE_ENV=development bun run --hot src/main.ts",
"start": "NODE_ENV=production bun run src/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",
"rate-limit-redis": "^4.2.0",
"yup": "^1.6.1",
"zod": "^3.24.3"
},
"devDependencies": {
"@types/bun": "^1.2.11",
"prettier": "^3.5.3"
}
}

View File

@ -1,65 +0,0 @@
import { createHandlers } from "./utils.ts";
import Argon2id from "@rabbit-company/argon2id";
import { object, string, ValidationError } from "yup";
import type { Context } from "hono";
import type { Bindings, BlankEnv, BlankInput } from "hono/types";
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(),
});
type ContextType = Context<BlankEnv & { Bindings: Bindings }, "/user", BlankInput>;
export const userExists = async (username: string, client: Client) => {
const query = `
SELECT * FROM users WHERE username = $1
`;
const result = await client.queryObject(query, [username]);
return result.rows.length > 0;
};
export const registerHandler = createHandlers(async (c: ContextType) => {
const client = c.get("dbCred");
try {
const body = await RegistrationBodySchema.validate(await c.req.json());
const { username, password, nickname } = body;
if (await userExists(username, client)) {
return c.json({
message: `User "${username}" already exists.`,
}, 400);
}
const hash = await Argon2id.hashEncoded(password);
const query = `
INSERT INTO users (username, password, nickname) VALUES ($1, $2, $3)
`;
await client.queryObject(query, [username, hash, nickname || null]);
return c.json({
message: `User "${username}" registered successfully.`,
}, 201);
} catch (e) {
if (e instanceof ValidationError) {
return c.json({
message: "Invalid registration data.",
errors: e.errors,
}, 400);
} else if (e instanceof SyntaxError) {
return c.json({
message: "Invalid JSON in request body.",
}, 400);
} else {
console.error("Registration error:", e);
return c.json({
message: "An unexpected error occurred during registration.",
error: (e as Error).message,
}, 500);
}
}
});

View File

@ -0,0 +1,10 @@
import { Context } from "hono";
export const notFoundRoute = (c: Context) => {
return c.json(
{
message: "Not Found"
},
404
);
};

View File

@ -0,0 +1,17 @@
import { rootHandler } from "./root/root.ts";
import { pingHandler } from "./ping.ts";
import { getSnapshotsHanlder } from "./snapshots.ts";
import { registerHandler } from "./user.ts";
import { videoInfoHandler } from "db/videoInfo.ts";
import { Hono } from "hono";
import { Variables } from "hono/types";
export function configureRoutes(app: Hono<{Variables: Variables }>) {
app.get("/", ...rootHandler);
app.all("/ping", ...pingHandler);
app.get("/video/:id/snapshots", ...getSnapshotsHanlder);
app.post("/user", ...registerHandler);
app.get("/video/:id/info", ...videoInfoHandler);
}

View File

@ -0,0 +1,24 @@
import { getClientIP } from "middleware/logger.ts";
import { createHandlers } from "../src/utils.ts";
import { VERSION } from "../src/main.ts";
export const pingHandler = createHandlers(async (c) => {
const requestHeaders = c.req.raw.headers;
return c.json({
"message": "pong",
"request": {
"headers": requestHeaders,
"ip": getClientIP(c),
"mode": c.req.raw.mode,
"method": c.req.method,
"query": new URL(c.req.url).searchParams,
"body": await c.req.text(),
"url": c.req.raw.url
},
"response": {
"time": new Date().getTime(),
"status": 200,
"version": VERSION,
}
});
});

View File

@ -1,6 +1,6 @@
import { getSingerForBirthday, pickSinger, pickSpecialSinger, type Singer } from "./singers.ts"; import { getSingerForBirthday, pickSinger, pickSpecialSinger, type Singer } from "./singers.ts";
import { VERSION } from "./main.ts"; import { VERSION } from "../../src/main.ts";
import { createHandlers } from "./utils.ts"; import { createHandlers } from "../../src/utils.ts";
export const rootHandler = createHandlers((c) => { export const rootHandler = createHandlers((c) => {
let singer: Singer | Singer[]; let singer: Singer | Singer[];
@ -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
}); });
}); });

View File

@ -0,0 +1,103 @@
export const singers = [
{
name: "洛天依",
color: "#66CCFF",
birthday: "0712"
},
{
name: "言和",
color: "#00FFCC",
birthday: "0711"
},
{
name: "乐正绫",
color: "#EE0000",
birthday: "0412"
},
{
name: "乐正龙牙",
color: "#006666",
birthday: "1002"
},
{
name: "徵羽摩柯",
color: "#0080FF",
birthday: "1210"
},
{
name: "墨清弦",
color: "#FFFF00",
birthday: "0520"
},
{
name: "星尘",
color: "#9999FF",
birthday: "0812"
},
{
name: "心华",
color: "#EE82EE",
birthday: "0210"
},
{
name: "海伊",
color: "#3399FF",
birthday: "0722"
},
{
name: "苍穹",
color: "#8BC0B5",
birthday: "0520"
},
{
name: "赤羽",
color: "#FF4004",
birthday: "1126"
},
{
name: "诗岸",
color: "#F6BE72",
birthday: "0119"
},
{
name: "牧心",
color: "#2A2859",
birthday: "0807"
}
];
export interface Singer {
name: string;
color?: string;
birthday?: string;
message?: string;
}
export const specialSingers = [
{
name: "雅音宫羽",
message: "你是我最真模样,从来不曾遗忘。"
},
{
name: "初音未来",
message: "初始之音,响彻未来!"
}
];
export const pickSinger = () => {
const index = Math.floor(Math.random() * singers.length);
return singers[index];
};
export const pickSpecialSinger = () => {
const index = Math.floor(Math.random() * specialSingers.length);
return specialSingers[index];
};
export const getSingerForBirthday = (): Singer[] => {
const today = new Date();
const month = String(today.getMonth() + 1).padStart(2, "0");
const day = String(today.getDate()).padStart(2, "0");
const datestring = `${month}${day}`;
return singers.filter((singer) => singer.birthday === datestring);
};

View File

@ -1,22 +1,24 @@
import type { Context } from "hono"; import type { Context } from "hono";
import { createHandlers } from "./utils.ts"; import { createHandlers } from "../src/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 "../src/schema";
import { startTime, endTime } from "hono/timing";
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 +36,12 @@ 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"); startTime(c, "parse", "Parse the request");
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;
@ -70,23 +71,36 @@ export const getSnapshotsHanlder = createHandlers(async (c: ContextType) => {
let result: VideoSnapshotType[]; let result: VideoSnapshotType[];
endTime(c, "parse");
startTime(c, "db", "Query the database");
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);
} }
endTime(c, "db");
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,74 @@
import { createHandlers } from "src/utils.ts";
import Argon2id from "@rabbit-company/argon2id";
import { object, string, ValidationError } from "yup";
import type { Context } from "hono";
import type { Bindings, BlankEnv, BlankInput } from "hono/types";
import { sqlCred } from "db/db.ts";
import { ErrorResponse, StatusResponse } from "src/schema";
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()
});
type ContextType = Context<BlankEnv & { Bindings: Bindings }, "/user", BlankInput>;
export const userExists = async (username: string) => {
const result = await sqlCred`
SELECT 1
FROM users
WHERE username = ${username}
`;
return result.length > 0;
};
export const registerHandler = createHandlers(async (c: ContextType) => {
try {
const body = await RegistrationBodySchema.validate(await c.req.json());
const { username, password, nickname } = body;
if (await userExists(username)) {
const response: StatusResponse = {
message: `User "${username}" already exists.`
};
return c.json<StatusResponse>(response, 400);
}
const hash = await Argon2id.hashEncoded(password);
await sqlCred`
INSERT INTO users (username, password, nickname)
VALUES (${username}, ${hash}, ${nickname ? nickname : null})
`;
const response: StatusResponse = {
message: `User '${username}' registered successfully.`
}
return c.json<StatusResponse>(response, 201);
} catch (e) {
if (e instanceof ValidationError) {
const response: ErrorResponse<string> = {
message: "Invalid registration data.",
errors: e.errors,
code: "INVALID_PAYLOAD"
}
return c.json<ErrorResponse<string>>(response, 400);
} else if (e instanceof SyntaxError) {
const response: ErrorResponse<string> = {
message: "Invalid JSON payload.",
errors: [e.message],
code: "INVALID_FORMAT"
}
return c.json<ErrorResponse<string>>(response, 400);
} else {
const response: ErrorResponse<string> = {
message: "Invalid JSON payload.",
errors: [(e as Error).message],
code: "UNKNOWN_ERR"
}
return c.json<ErrorResponse<string>>(response, 500);
}
}
});

View File

@ -1,103 +0,0 @@
export const singers = [
{
"name": "洛天依",
"color": "#66CCFF",
"birthday": "0712",
},
{
"name": "言和",
"color": "#00FFCC",
"birthday": "0711",
},
{
"name": "乐正绫",
"color": "#EE0000",
"birthday": "0412",
},
{
"name": "乐正龙牙",
"color": "#006666",
"birthday": "1002",
},
{
"name": "徵羽摩柯",
"color": "#0080FF",
"birthday": "1210",
},
{
"name": "墨清弦",
"color": "#FFFF00",
"birthday": "0520",
},
{
"name": "星尘",
"color": "#9999FF",
"birthday": "0812",
},
{
"name": "心华",
"color": "#EE82EE",
"birthday": "0210",
},
{
"name": "海伊",
"color": "#3399FF",
"birthday": "0722",
},
{
"name": "苍穹",
"color": "#8BC0B5",
"birthday": "0520",
},
{
"name": "赤羽",
"color": "#FF4004",
"birthday": "1126",
},
{
"name": "诗岸",
"color": "#F6BE72",
"birthday": "0119",
},
{
"name": "牧心",
"color": "#2A2859",
"birthday": "0807",
},
];
export interface Singer {
name: string;
color?: string;
birthday?: string;
message?: string;
}
export const specialSingers = [
{
"name": "雅音宫羽",
"message": "你是我最真模样,从来不曾遗忘。",
},
{
"name": "初音未来",
"message": "初始之音,响彻未来!",
},
];
export const pickSinger = () => {
const index = Math.floor(Math.random() * singers.length);
return singers[index];
};
export const pickSpecialSinger = () => {
const index = Math.floor(Math.random() * specialSingers.length);
return specialSingers[index];
};
export const getSingerForBirthday = (): Singer[] => {
const today = new Date();
const month = String(today.getMonth() + 1).padStart(2, "0");
const day = String(today.getDate()).padStart(2, "0");
const datestring = `${month}${day}`;
return singers.filter((singer) => singer.birthday === datestring);
};

View File

@ -0,0 +1,18 @@
import { Hono } from "hono";
import type { TimingVariables } from "hono/timing";
import { startServer } from "./startServer.ts";
import { configureRoutes } from "routes";
import { configureMiddleWares } from "middleware";
import { notFoundRoute } from "routes/404.ts";
type Variables = TimingVariables;
const app = new Hono<{ Variables: Variables }>();
app.notFound(notFoundRoute);
configureMiddleWares(app);
configureRoutes(app);
await startServer(app);
export const VERSION = "0.4.6";

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

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

View File

@ -0,0 +1,91 @@
import { serve } from "bun";
import { Hono } from "hono";
import os from "os";
import { BlankSchema, Variables } from "hono/types";
function getLocalIpAddress(): string {
const interfaces = os.networkInterfaces();
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]!) {
if (iface.family === "IPv4" && !iface.internal) {
return iface.address;
}
}
}
return "localhost";
}
function logStartup(hostname: string, port: number, wasAutoIncremented: boolean, originalPort: number) {
const localUrl = `http://localhost:${port}`;
const networkIp = hostname === "0.0.0.0" ? getLocalIpAddress() : "";
const networkUrl = networkIp ? `http://${networkIp}:${port}` : "";
console.log("\n");
console.log("🚀 Server is running at:");
console.log(`> Local: ${localUrl}`);
if (networkIp) {
console.log(`> Network: ${networkUrl}`);
}
if (wasAutoIncremented) {
console.log(`\n⚠ Port ${originalPort} is in use, using port ${port} instead.`);
}
console.log("\nPress Ctrl+C to quit.");
}
export async function startServer(app: Hono<{Variables: Variables }>) {
const NODE_ENV = process.env.NODE_ENV || "production";
const HOST = process.env.HOST ?? (NODE_ENV === "development" ? "0.0.0.0" : "127.0.0.1");
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
const DEFAULT_PORT = 3000;
const MAX_ATTEMPTS = 15;
if (PORT !== undefined) {
try {
const server = serve({
fetch: app.fetch,
hostname: HOST,
port: PORT
});
logStartup(HOST, PORT, false, DEFAULT_PORT);
return server;
} catch (e: any) {
console.error(`Failed to start server on port ${PORT}:`, e.message);
process.exit(1);
}
}
let attemptPort = DEFAULT_PORT;
let success = false;
let error: unknown = null;
for (let i = 0; i <= MAX_ATTEMPTS; i++) {
try {
const server = serve({
fetch: app.fetch,
hostname: HOST,
port: attemptPort
});
const wasAutoIncremented = attemptPort !== DEFAULT_PORT;
logStartup(HOST, attemptPort, wasAutoIncremented, DEFAULT_PORT);
return server;
} catch (e: any) {
if (e.code === "EADDRINUSE") {
attemptPort++;
} else {
error = e;
break;
}
}
}
if (!success) {
console.error(`Could not find an available port after ${MAX_ATTEMPTS + 1} attempts.`);
if (error) {
console.error("Last error:", (error as Error).message);
}
process.exit(1);
}
}

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
}
}

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

@ -0,0 +1,104 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": {
"chalk": "^5.4.1",
"ioredis": "^5.6.1",
"logform": "^2.7.0",
"postgres": "^3.4.5",
"winston": "^3.17.0",
},
"devDependencies": {
"@types/ioredis": "^5.0.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=="],
"@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=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"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-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=="],
"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=="],
"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=="],
"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-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"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=="],
"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=="],
"postgres": ["postgres@3.4.5", "", {}, "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg=="],
"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-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=="],
"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=="],
"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

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

View File

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

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,3 +1,10 @@
import { Redis } from "ioredis"; import { Redis } from "ioredis";
export const redis = new Redis({ maxRetriesPerRequest: null }); const host = process.env.REDIS_HOST || "localhost";
const port = parseInt(process.env.REDIS_PORT) || 6379;
export const redis = new Redis({
port: port,
host: host,
maxRetriesPerRequest: null,
});

View File

@ -1,6 +1,6 @@
export interface AllDataType { export interface AllDataType {
id: number; id: number;
aid: bigint; aid: number;
bvid: string | null; bvid: string | null;
description: string | null; description: string | null;
uid: number | null; uid: number | null;
@ -28,12 +28,12 @@ export interface VideoSnapshotType {
favorites: number; favorites: number;
shares: number; shares: number;
danmakus: number; danmakus: number;
aid: bigint; aid: number;
replies: number; replies: number;
} }
export interface LatestSnapshotType { export interface LatestSnapshotType {
aid: bigint; aid: number;
time: number; time: number;
views: number; views: number;
danmakus: number; danmakus: number;
@ -46,7 +46,7 @@ export interface LatestSnapshotType {
export interface SnapshotScheduleType { export interface SnapshotScheduleType {
id: number; id: number;
aid: bigint; aid: number;
type?: string; type?: string;
created_at: string; created_at: string;
started_at?: string; started_at?: string;

View File

@ -7,6 +7,11 @@
"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/": "./",
"child_process": "node:child_process",
"util": "node:util"
} }
} }

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) => {
@ -24,13 +24,13 @@ const createTransport = (level: string, filename: string) => {
let maxsize = undefined; let maxsize = undefined;
let maxFiles = undefined; let maxFiles = undefined;
let tailable = undefined; let tailable = undefined;
if (level === "verbose") { if (level === "silly") {
maxsize = 10 * MB; maxsize = 500 * MB;
maxFiles = 10; maxFiles = undefined;
tailable = false; tailable = false;
} else if (level === "warn") { } else if (level === "warn") {
maxsize = 10 * MB; maxsize = 10 * MB;
maxFiles = 1; maxFiles = 5;
tailable = false; tailable = false;
} }
function replacer(key: unknown, value: unknown) { function replacer(key: unknown, value: unknown) {
@ -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,
@ -62,7 +62,7 @@ const winstonLogger = winston.createLogger({
new transports.Console({ new transports.Console({
level: "debug", level: "debug",
format: format.combine( format: format.combine(
format.timestamp({ format: "HH:mm:ss.SSS" }), format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }),
format.colorize(), format.colorize(),
format.errors({ stack: true }), format.errors({ stack: true }),
customFormat, customFormat,

View File

@ -1,4 +1,4 @@
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
logger.error(Error("test error"), "test service"); logger.error(Error("test error"), "test service");
logger.debug(`some string`); logger.debug(`some string`);

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,9 +1,43 @@
import logger from "log/logger.ts"; import logger from "@core/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 { spawn, SpawnOptions } from "child_process";
export function spawnPromise(
command: string,
args: string[] = [],
options?: SpawnOptions,
): Promise<{ stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
const child = spawn(command, args, options);
let stdout = "";
let stderr = "";
child.stdout?.on("data", (data) => {
stdout += data;
});
child.stderr?.on("data", (data) => {
stderr += data;
});
child.on("close", (code) => {
if (code !== 0) {
reject(new Error(`Error code: ${code}\nstderr: ${stderr}`));
} else {
resolve({ stdout, stderr });
}
});
child.on("error", (err) => {
reject(err);
});
});
}
interface Proxy { interface Proxy {
type: string; type: string;
@ -99,7 +133,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 +243,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;
} }
@ -230,7 +264,7 @@ class NetworkDelegate {
clearTimeout(timeout); clearTimeout(timeout);
return await response.json() as R; return (await response.json()) as R;
} catch (e) { } catch (e) {
throw new NetSchedulerError("Fetch error", "FETCH_ERROR", e); throw new NetSchedulerError("Fetch error", "FETCH_ERROR", e);
} }
@ -238,9 +272,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 output = await spawnPromise("aliyun", [
const output = await new Deno.Command("aliyun", {
args: [
"fc", "fc",
"POST", "POST",
`/2023-03-30/functions/proxy-${region}/invocations`, `/2023-03-30/functions/proxy-${region}/invocations`,
@ -258,9 +290,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
@ -269,11 +300,15 @@ class NetworkDelegate {
"ALICLOUD_PROXY_ERR", "ALICLOUD_PROXY_ERR",
); );
} else { } else {
return JSON.parse(JSON.parse(rawData.body)) as R; return JSON.parse(rawData.body) as R;
} }
} catch (e) { } catch (e) {
logger.error(e as Error, "net", "fn:alicloudFcRequest"); logger.error(e as Error, "net", "fn:alicloudFcRequest");
throw new NetSchedulerError(`Unhandled error: Cannot proxy ${url} to ali-fc-${region}.`, "ALICLOUD_PROXY_ERR", e); throw new NetSchedulerError(
`Unhandled error: Cannot proxy ${url} to ali-fc-${region}.`,
"ALICLOUD_PROXY_ERR",
e,
);
} }
} }
} }
@ -358,7 +393,11 @@ for (const region of regions) {
} }
networkDelegate.addTask("getVideoInfo", "bilibili", "all"); networkDelegate.addTask("getVideoInfo", "bilibili", "all");
networkDelegate.addTask("getLatestVideos", "bilibili", "all"); networkDelegate.addTask("getLatestVideos", "bilibili", "all");
networkDelegate.addTask("snapshotMilestoneVideo", "bilibili", regions.map((region) => `alicloud-${region}`)); networkDelegate.addTask(
"snapshotMilestoneVideo",
"bilibili",
regions.map((region) => `alicloud-${region}`),
);
networkDelegate.addTask("snapshotVideo", "bili_test", [ networkDelegate.addTask("snapshotVideo", "bili_test", [
"alicloud-qingdao", "alicloud-qingdao",
"alicloud-shanghai", "alicloud-shanghai",

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,13 @@
{
"name": "core",
"dependencies": {
"chalk": "^5.4.1",
"ioredis": "^5.6.1",
"logform": "^2.7.0",
"postgres": "^3.4.5",
"winston": "^3.17.0"
},
"devDependencies": {
"@types/ioredis": "^5.0.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/*": ["./*"]
},
"allowSyntheticDefaultImports": true,
"allowImportingTsExtensions": true,
"noEmit": true
}
}

2
packages/crawler/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.cache
build/

433
packages/crawler/bun.lock Normal file
View File

@ -0,0 +1,433 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "core",
"dependencies": {
"@bull-board/api": "^6.9.5",
"@bull-board/express": "^6.9.5",
"@huggingface/transformers": "^3.5.1",
"bullmq": "^5.52.1",
"express": "^5.1.0",
"ioredis": "^5.6.1",
"onnxruntime-node": "1.19.2",
},
"devDependencies": {
"concurrently": "^9.1.2",
},
},
},
"packages": {
"@bull-board/api": ["@bull-board/api@6.9.5", "", { "dependencies": { "redis-info": "^3.1.0" }, "peerDependencies": { "@bull-board/ui": "6.9.5" } }, "sha512-9d03Mu9fuQ3sHAxAwUmlTo8C9KwKHd6Ef2IUOZMG93cAHsQ0oaOpYc5d0roZacYFvfYKr7bTANptKO1f3GLp2g=="],
"@bull-board/express": ["@bull-board/express@6.9.5", "", { "dependencies": { "@bull-board/api": "6.9.5", "@bull-board/ui": "6.9.5", "ejs": "^3.1.10", "express": "^4.21.1 || ^5.0.0" } }, "sha512-N2B/RSEX/EpIvT/uOJXCACevElv2CUEqrvgoLuwM2n1Fb3VWaatj/S004VLUtHjOCPfODosJ1xoxelelQcV3Dw=="],
"@bull-board/ui": ["@bull-board/ui@6.9.5", "", { "dependencies": { "@bull-board/api": "6.9.5" } }, "sha512-+4YnDvyuY3MOVkXFxkspRbqawLtIExHzRQ4raQWagOc35KD7v2/ccFGyRPDI/N0bemsiNkOPkcZGf/LFcaOZmA=="],
"@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
"@huggingface/jinja": ["@huggingface/jinja@0.4.1", "", {}, "sha512-3WXbMFaPkk03LRCM0z0sylmn8ddDm4ubjU7X+Hg4M2GOuMklwoGAFXp9V2keq7vltoB/c7McE5aHUVVddAewsw=="],
"@huggingface/transformers": ["@huggingface/transformers@3.5.1", "", { "dependencies": { "@huggingface/jinja": "^0.4.1", "onnxruntime-node": "1.21.0", "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4", "sharp": "^0.34.1" } }, "sha512-qWsPoJMBPYcrGuzRMVL//3dwcLXED9r+j+Hzw+hZpBfrMPdyq57ehNs7lew0D34Paj0DO0E0kZQjOZcIVN17hQ=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.1.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.1.0" }, "os": "darwin", "cpu": "x64" }, "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.1.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.1.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.1.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.1.0" }, "os": "linux", "cpu": "arm" }, "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.1.0" }, "os": "linux", "cpu": "s390x" }, "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.1", "", { "dependencies": { "@emnapi/runtime": "^1.4.0" }, "cpu": "none" }, "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.1", "", { "os": "win32", "cpu": "x64" }, "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw=="],
"@ioredis/commands": ["@ioredis/commands@1.2.0", "", {}, "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="],
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="],
"@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="],
"@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="],
"@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="],
"@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="],
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="],
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
"@types/node": ["@types/node@22.15.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"bullmq": ["bullmq@5.52.1", "", { "dependencies": { "cron-parser": "^4.9.0", "ioredis": "^5.4.1", "msgpackr": "^1.11.2", "node-abort-controller": "^3.1.1", "semver": "^7.5.4", "tslib": "^2.0.0", "uuid": "^9.0.0" } }, "sha512-u7CSV9wID3MBEX2DNubEErbAlrADgm8abUBAi6h8rQTnuTkhhgMs2iD7uhqplK8lIgUOkBIW3sDJWaMSInH47A=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"concurrently": ["concurrently@9.1.2", "", { "dependencies": { "chalk": "^4.1.2", "lodash": "^4.17.21", "rxjs": "^7.8.1", "shell-quote": "^1.8.1", "supports-color": "^8.1.1", "tree-kill": "^1.2.2", "yargs": "^17.7.2" }, "bin": { "concurrently": "dist/bin/concurrently.js", "conc": "dist/bin/concurrently.js" } }, "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ=="],
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="],
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="],
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
"flatbuffers": ["flatbuffers@25.2.10", "", {}, "sha512-7JlN9ZvLDG1McO3kbX0k4v+SUAg48L1rIwEvN6ZQl/eCtgJz9UylTMzE9wrmYrcorgxm3CX/3T/w5VAub99UUw=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"guid-typescript": ["guid-typescript@1.0.9", "", {}, "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"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=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"jake": ["jake@10.9.2", "", { "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", "filelist": "^1.0.4", "minimatch": "^3.1.2" }, "bin": { "jake": "bin/cli.js" } }, "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA=="],
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="],
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"msgpackr": ["msgpackr@1.11.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g=="],
"msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="],
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"onnxruntime-common": ["onnxruntime-common@1.19.2", "", {}, "sha512-a4R7wYEVFbZBlp0BfhpbFWqe4opCor3KM+5Wm22Az3NGDcQMiU2hfG/0MfnBs+1ZrlSGmlgWeMcXQkDk1UFb8Q=="],
"onnxruntime-node": ["onnxruntime-node@1.19.2", "", { "dependencies": { "onnxruntime-common": "1.19.2", "tar": "^7.0.1" }, "os": [ "linux", "win32", "darwin", ] }, "sha512-9eHMP/HKbbeUcqte1JYzaaRC8JPn7ojWeCeoyShO86TOR97OCyIyAIOGX3V95ErjslVhJRXY8Em/caIUc0hm1Q=="],
"onnxruntime-web": ["onnxruntime-web@1.22.0-dev.20250409-89f8206ba4", "", { "dependencies": { "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
"platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="],
"protobufjs": ["protobufjs@7.5.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-Z2E/kOY1QjoMlCytmexzYfDm/w5fKAiRwpSzGtdnXW1zC88Z2yXazHHrOtwCzn+7wSxyE8PYM4rvVcMphF9sOA=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
"redis-info": ["redis-info@3.1.0", "", { "dependencies": { "lodash": "^4.17.11" } }, "sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg=="],
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"sharp": ["sharp@0.34.1", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.7.1" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.1", "@img/sharp-darwin-x64": "0.34.1", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.1", "@img/sharp-linux-arm64": "0.34.1", "@img/sharp-linux-s390x": "0.34.1", "@img/sharp-linux-x64": "0.34.1", "@img/sharp-linuxmusl-arm64": "0.34.1", "@img/sharp-linuxmusl-x64": "0.34.1", "@img/sharp-wasm32": "0.34.1", "@img/sharp-win32-ia32": "0.34.1", "@img/sharp-win32-x64": "0.34.1" } }, "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg=="],
"shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"@huggingface/transformers/onnxruntime-node": ["onnxruntime-node@1.21.0", "", { "dependencies": { "global-agent": "^3.0.0", "onnxruntime-common": "1.21.0", "tar": "^7.0.1" }, "os": [ "linux", "win32", "darwin", ] }, "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw=="],
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
"onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.22.0-dev.20250409-89f8206ba4", "", {}, "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ=="],
"@huggingface/transformers/onnxruntime-node/onnxruntime-common": ["onnxruntime-common@1.21.0", "", {}, "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ=="],
"filelist/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
}
}

View File

@ -1,48 +1,45 @@
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts"; import type { Psql } from "global.d.ts";
import { AllDataType, BiliUserType } from "@core/db/schema"; import { AllDataType, BiliUserType } from "@core/db/schema";
import Akari from "ml/akari.ts"; import { AkariModelVersion } from "ml/const";
export async function videoExistsInAllData(client: Client, aid: number) { export async function videoExistsInAllData(sql: Psql, aid: number) {
return await client.queryObject<{ exists: boolean }>( const rows = await sql<{ exists: boolean }[]>`
`SELECT EXISTS(SELECT 1 FROM bilibili_metadata WHERE aid = $1)`, SELECT EXISTS(SELECT 1 FROM bilibili_metadata WHERE aid = ${aid})
[aid], `;
) return rows[0].exists;
.then((result) => result.rows[0].exists);
} }
export async function userExistsInBiliUsers(client: Client, uid: number) { export async function userExistsInBiliUsers(sql: Psql, uid: number) {
return await client.queryObject<{ exists: boolean }>(`SELECT EXISTS(SELECT 1 FROM bilibili_user WHERE uid = $1)`, [ const rows = await sql<{ exists: boolean }[]>`
uid, SELECT EXISTS(SELECT 1 FROM bilibili_user WHERE uid = ${uid})
]); `;
return rows[0].exists;
} }
export async function getUnlabelledVideos(client: Client) { export async function getUnlabelledVideos(sql: Psql) {
const queryResult = await client.queryObject<{ aid: number }>( const rows = await sql<{ aid: number }[]>`
`SELECT a.aid FROM bilibili_metadata a LEFT JOIN labelling_result l ON a.aid = l.aid WHERE l.aid IS NULL`, SELECT a.aid FROM bilibili_metadata a LEFT JOIN labelling_result l ON a.aid = l.aid WHERE l.aid IS NULL
); `;
return queryResult.rows.map((row) => row.aid); return rows.map((row) => row.aid);
} }
export async function insertVideoLabel(client: Client, aid: number, label: number) { export async function insertVideoLabel(sql: Psql, aid: number, label: number) {
return await client.queryObject( await sql`
`INSERT INTO labelling_result (aid, label, model_version) VALUES ($1, $2, $3) ON CONFLICT (aid, model_version) DO NOTHING`, INSERT INTO labelling_result (aid, label, model_version) VALUES (${aid}, ${label}, ${AkariModelVersion}) ON CONFLICT (aid, model_version) DO NOTHING
[aid, label, Akari.getModelVersion()], `;
);
} }
export async function getVideoInfoFromAllData(client: Client, aid: number) { export async function getVideoInfoFromAllData(sql: Psql, aid: number) {
const queryResult = await client.queryObject<AllDataType>( const rows = await sql<AllDataType[]>`
`SELECT * FROM bilibili_metadata WHERE aid = $1`, SELECT * FROM bilibili_metadata WHERE aid = ${aid}
[aid], `;
); const row = rows[0];
const row = queryResult.rows[0];
let authorInfo = ""; let authorInfo = "";
if (row.uid && await userExistsInBiliUsers(client, row.uid)) { if (row.uid && await userExistsInBiliUsers(sql, row.uid)) {
const q = await client.queryObject<BiliUserType>( const userRows = await sql<BiliUserType[]>`
`SELECT * FROM bilibili_user WHERE uid = $1`, SELECT * FROM bilibili_user WHERE uid = ${row.uid}
[row.uid], `;
); const userRow = userRows[0];
const userRow = q.rows[0];
if (userRow) { if (userRow) {
authorInfo = userRow.desc; authorInfo = userRow.desc;
} }
@ -55,33 +52,26 @@ export async function getVideoInfoFromAllData(client: Client, aid: number) {
}; };
} }
export async function getUnArchivedBiliUsers(client: Client) { export async function getUnArchivedBiliUsers(sql: Psql) {
const queryResult = await client.queryObject<{ uid: number }>( const rows = await sql<{ uid: number }[]>`
`
SELECT ad.uid SELECT ad.uid
FROM bilibili_metadata ad FROM bilibili_metadata ad
LEFT JOIN bilibili_user bu ON ad.uid = bu.uid LEFT JOIN bilibili_user bu ON ad.uid = bu.uid
WHERE bu.uid IS NULL; WHERE bu.uid IS NULL;
`, `;
[],
);
const rows = queryResult.rows;
return rows.map((row) => row.uid); return rows.map((row) => row.uid);
} }
export async function setBiliVideoStatus(client: Client, aid: number, status: number) { export async function setBiliVideoStatus(sql: Psql, aid: number, status: number) {
return await client.queryObject( await sql`
`UPDATE bilibili_metadata SET status = $1 WHERE aid = $2`, UPDATE bilibili_metadata SET status = ${status} WHERE aid = ${aid}
[status, aid], `;
);
} }
export async function getBiliVideoStatus(client: Client, aid: number) { export async function getBiliVideoStatus(sql: Psql, aid: number) {
const queryResult = await client.queryObject<{ status: number }>( const rows = await sql<{ status: number }[]>`
`SELECT status FROM bilibili_metadata WHERE aid = $1`, SELECT status FROM bilibili_metadata WHERE aid = ${aid}
[aid], `;
);
const rows = queryResult.rows;
if (rows.length === 0) return 0; if (rows.length === 0) return 0;
return rows[0].status; return rows[0].status;
} }

View File

@ -1,6 +0,0 @@
import { Pool } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
import { postgresConfig } from "@core/db/pgConfig";
const pool = new Pool(postgresConfig, 12);
export const db = pool;

View File

@ -1,9 +1,9 @@
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
import { LatestSnapshotType } from "@core/db/schema"; import { LatestSnapshotType } from "@core/db/schema";
import { SnapshotNumber } from "mq/task/getVideoStats.ts"; import { SnapshotNumber } from "mq/task/getVideoStats.ts";
import type { Psql } from "global.d.ts";
export async function getVideosNearMilestone(client: Client) { export async function getVideosNearMilestone(sql: Psql) {
const queryResult = await client.queryObject<LatestSnapshotType>(` const queryResult = await sql<LatestSnapshotType[]>`
SELECT ls.* SELECT ls.*
FROM latest_video_snapshot ls FROM latest_video_snapshot ls
RIGHT JOIN songs ON songs.aid = ls.aid RIGHT JOIN songs ON songs.aid = ls.aid
@ -18,8 +18,8 @@ export async function getVideosNearMilestone(client: Client) {
(views >= 90000 AND views < 100000) OR (views >= 90000 AND views < 100000) OR
(views >= 900000 AND views < 1000000) OR (views >= 900000 AND views < 1000000) OR
(views >= 9900000 AND views < 10000000) (views >= 9900000 AND views < 10000000)
`); `;
return queryResult.rows.map((row) => { return queryResult.map((row) => {
return { return {
...row, ...row,
aid: Number(row.aid), aid: Number(row.aid),
@ -27,19 +27,16 @@ export async function getVideosNearMilestone(client: Client) {
}); });
} }
export async function getLatestVideoSnapshot(client: Client, aid: number): Promise<null | SnapshotNumber> { export async function getLatestVideoSnapshot(sql: Psql, aid: number): Promise<null | SnapshotNumber> {
const queryResult = await client.queryObject<LatestSnapshotType>( const queryResult = await sql<LatestSnapshotType[]>`
`
SELECT * SELECT *
FROM latest_video_snapshot FROM latest_video_snapshot
WHERE aid = $1 WHERE aid = ${aid}
`, `;
[aid], if (queryResult.length === 0) {
);
if (queryResult.rows.length === 0) {
return null; return null;
} }
return queryResult.rows.map((row) => { return queryResult.map((row) => {
return { return {
...row, ...row,
aid: Number(row.aid), aid: Number(row.aid),

View File

@ -1,10 +1,10 @@
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts"; import type { SnapshotScheduleType } from "@core/db/schema.d.ts";
import { SnapshotScheduleType } from "@core/db/schema"; import logger from "@core/log/logger.ts";
import logger from "log/logger.ts"; import { MINUTE } from "@core/const/time.ts";
import { MINUTE } from "$std/datetime/constants.ts";
import { redis } from "@core/db/redis.ts"; import { redis } from "@core/db/redis.ts";
import { Redis } from "ioredis"; import { Redis } from "ioredis";
import { parseTimestampFromPsql } from "../utils/formatTimestampToPostgre.ts"; import { parseTimestampFromPsql } from "../utils/formatTimestampToPostgre.ts";
import type { Psql } from "global.d.ts";
const REDIS_KEY = "cvsa:snapshot_window_counts"; const REDIS_KEY = "cvsa:snapshot_window_counts";
@ -14,11 +14,11 @@ function getCurrentWindowIndex(): number {
return Math.floor(minutesSinceMidnight / 5); return Math.floor(minutesSinceMidnight / 5);
} }
export async function refreshSnapshotWindowCounts(client: Client, redisClient: Redis) { export async function refreshSnapshotWindowCounts(sql: Psql, redisClient: Redis) {
const now = new Date(); const now = new Date();
const startTime = now.getTime(); const startTime = now.getTime();
const result = await client.queryObject<{ window_start: Date; count: number }>` const result = await sql<{ window_start: Date; count: number }[]>`
SELECT SELECT
date_trunc('hour', started_at) + date_trunc('hour', started_at) +
(EXTRACT(minute FROM started_at)::int / 5 * INTERVAL '5 minutes') AS window_start, (EXTRACT(minute FROM started_at)::int / 5 * INTERVAL '5 minutes') AS window_start,
@ -33,7 +33,7 @@ export async function refreshSnapshotWindowCounts(client: Client, redisClient: R
const currentWindow = getCurrentWindowIndex(); const currentWindow = getCurrentWindowIndex();
for (const row of result.rows) { for (const row of result) {
const targetOffset = Math.floor((row.window_start.getTime() - startTime) / (5 * MINUTE)); const targetOffset = Math.floor((row.window_start.getTime() - startTime) / (5 * MINUTE));
const offset = currentWindow + targetOffset; const offset = currentWindow + targetOffset;
if (offset >= 0) { if (offset >= 0) {
@ -42,10 +42,10 @@ export async function refreshSnapshotWindowCounts(client: Client, redisClient: R
} }
} }
export async function initSnapshotWindowCounts(client: Client, redisClient: Redis) { export async function initSnapshotWindowCounts(sql: Psql, redisClient: Redis) {
await refreshSnapshotWindowCounts(client, redisClient); await refreshSnapshotWindowCounts(sql, redisClient);
setInterval(async () => { setInterval(async () => {
await refreshSnapshotWindowCounts(client, redisClient); await refreshSnapshotWindowCounts(sql, redisClient);
}, 5 * MINUTE); }, 5 * MINUTE);
} }
@ -54,44 +54,56 @@ async function getWindowCount(redisClient: Redis, offset: number): Promise<numbe
return count ? parseInt(count, 10) : 0; return count ? parseInt(count, 10) : 0;
} }
export async function snapshotScheduleExists(client: Client, id: number) { export async function snapshotScheduleExists(sql: Psql, id: number) {
const res = await client.queryObject<{ id: number }>( const rows = await sql<{ id: number }[]>`
`SELECT id FROM snapshot_schedule WHERE id = $1`, SELECT id
[id], FROM snapshot_schedule
); WHERE id = ${id}
return res.rows.length > 0; `;
return rows.length > 0;
} }
export async function videoHasActiveSchedule(client: Client, aid: number) { export async function videoHasActiveSchedule(sql: Psql, aid: number) {
const res = await client.queryObject<{ status: string }>( const rows = await sql<{ status: string }[]>`
`SELECT status FROM snapshot_schedule WHERE aid = $1 AND (status = 'pending' OR status = 'processing')`, SELECT status
[aid], FROM snapshot_schedule
); WHERE aid = ${aid}
return res.rows.length > 0; AND (status = 'pending'
OR status = 'processing'
)
`
return rows.length > 0;
} }
export async function videoHasActiveScheduleWithType(client: Client, aid: number, type: string) { export async function videoHasActiveScheduleWithType(sql: Psql, aid: number, type: string) {
const res = await client.queryObject<{ status: string }>( const rows = await sql<{ status: string }[]>`
`SELECT status FROM snapshot_schedule WHERE aid = $1 AND (status = 'pending' OR status = 'processing') AND type = $2`, SELECT status FROM snapshot_schedule
[aid, type], WHERE aid = ${aid}
); AND (status = 'pending' OR status = 'processing')
return res.rows.length > 0; AND type = ${type}
`;
return rows.length > 0;
} }
export async function videoHasProcessingSchedule(client: Client, aid: number) { export async function videoHasProcessingSchedule(sql: Psql, aid: number) {
const res = await client.queryObject<{ status: string }>( const rows = await sql<{ status: string }[]>`
`SELECT status FROM snapshot_schedule WHERE aid = $1 AND status = 'processing'`, SELECT status
[aid], FROM snapshot_schedule
); WHERE aid = ${aid}
return res.rows.length > 0; AND status = 'processing'
`
return rows.length > 0;
} }
export async function bulkGetVideosWithoutProcessingSchedules(client: Client, aids: number[]) { export async function bulkGetVideosWithoutProcessingSchedules(sql: Psql, aids: number[]) {
const res = await client.queryObject<{ aid: number }>( const rows = await sql<{ aid: string }[]>`
`SELECT aid FROM snapshot_schedule WHERE aid = ANY($1) AND status != 'processing' GROUP BY aid`, SELECT aid
[aids], FROM snapshot_schedule
); WHERE aid = ANY(${aids})
return res.rows.map((row) => row.aid); AND status != 'processing'
GROUP BY aid
`
return rows.map((row) => Number(row.aid));
} }
interface Snapshot { interface Snapshot {
@ -99,118 +111,106 @@ interface Snapshot {
views: number; views: number;
} }
export async function findClosestSnapshot( export async function findClosestSnapshot(sql: Psql, aid: number, targetTime: Date): Promise<Snapshot | null> {
client: Client, const result = await sql<{ created_at: string; views: number }[]>`
aid: number,
targetTime: Date,
): Promise<Snapshot | null> {
const query = `
SELECT created_at, views SELECT created_at, views
FROM video_snapshot FROM video_snapshot
WHERE aid = $1 WHERE aid = ${aid}
ORDER BY ABS(EXTRACT(EPOCH FROM (created_at - $2::timestamptz))) ORDER BY ABS(EXTRACT(EPOCH FROM (created_at - ${targetTime.toISOString()}::timestamptz)))
LIMIT 1 LIMIT 1
`; `;
const result = await client.queryObject<{ created_at: string; views: number }>( if (result.length === 0) return null;
query, const row = result[0];
[aid, targetTime.toISOString()],
);
if (result.rows.length === 0) return null;
const row = result.rows[0];
return { return {
created_at: new Date(row.created_at).getTime(), created_at: new Date(row.created_at).getTime(),
views: row.views, views: row.views
}; };
} }
export async function findSnapshotBefore( export async function findSnapshotBefore(sql: Psql, aid: number, targetTime: Date): Promise<Snapshot | null> {
client: Client, const result = await sql<{ created_at: string; views: number }[]>`
aid: number,
targetTime: Date,
): Promise<Snapshot | null> {
const query = `
SELECT created_at, views SELECT created_at, views
FROM video_snapshot FROM video_snapshot
WHERE aid = $1 WHERE aid = ${aid}
AND created_at <= $2::timestamptz AND created_at <= ${targetTime}::timestamptz
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 1 LIMIT 1
`; `;
const result = await client.queryObject<{ created_at: string; views: number }>( if (result.length === 0) return null;
query, const row = result[0];
[aid, targetTime.toISOString()],
);
if (result.rows.length === 0) return null;
const row = result.rows[0];
return { return {
created_at: new Date(row.created_at).getTime(), created_at: new Date(row.created_at).getTime(),
views: row.views, views: row.views
}; };
} }
export async function hasAtLeast2Snapshots(client: Client, aid: number) { export async function hasAtLeast2Snapshots(sql: Psql, aid: number) {
const res = await client.queryObject<{ count: number }>( const res = await sql<{ count: number }[]>`
`SELECT COUNT(*) FROM video_snapshot WHERE aid = $1`, SELECT COUNT(*)
[aid], FROM video_snapshot
); WHERE aid = ${aid}
return res.rows[0].count >= 2; `;
return res[0].count >= 2;
} }
export async function getLatestSnapshot(client: Client, aid: number): Promise<Snapshot | null> { export async function getLatestSnapshot(sql: Psql, aid: number): Promise<Snapshot | null> {
const res = await client.queryObject<{ created_at: string; views: number }>( const res = await sql<{ created_at: string; views: number }[]>`
`SELECT created_at, views FROM video_snapshot WHERE aid = $1 ORDER BY created_at DESC LIMIT 1`, SELECT created_at, views
[aid], FROM video_snapshot
); WHERE aid = ${aid}
if (res.rows.length === 0) return null; ORDER BY created_at DESC
const row = res.rows[0]; LIMIT 1
`;
if (res.length === 0) return null;
const row = res[0];
return { return {
created_at: new Date(row.created_at).getTime(), created_at: new Date(row.created_at).getTime(),
views: row.views, views: row.views
}; };
} }
export async function getLatestActiveScheduleWithType(client: Client, aid: number, type: string) { export async function getLatestActiveScheduleWithType(sql: Psql, aid: number, type: string) {
const query: string = ` const rows = await sql`
SELECT * SELECT *
FROM snapshot_schedule FROM snapshot_schedule
WHERE aid = $1 WHERE aid = ${aid}
AND type = $2 AND type = ${type}
AND (status = 'pending' OR status = 'processing') AND (status = 'pending' OR status = 'processing')
ORDER BY started_at DESC ORDER BY started_at DESC
LIMIT 1 LIMIT 1
` `;
const res = await client.queryObject<SnapshotScheduleType>(query, [aid, type]); return rows[0];
return res.rows[0];
} }
/* /*
* Creates a new snapshot schedule record. * Creates a new snapshot schedule record.
* @param client The database client.
* @param aid The aid of the video. * @param aid The aid of the video.
* @param type Type of the snapshot.
* @param targetTime Scheduled time for snapshot. (Timestamp in milliseconds) * @param targetTime Scheduled time for snapshot. (Timestamp in milliseconds)
* @param force Ignore all restrictions and force the creation of the schedule.
*/ */
export async function scheduleSnapshot( export async function scheduleSnapshot(
client: Client, sql: Psql,
aid: number, aid: number,
type: string, type: string,
targetTime: number, targetTime: number,
force: boolean = false, force: boolean = false
) { ) {
let adjustedTime = new Date(targetTime); let adjustedTime = new Date(targetTime);
const hashActiveSchedule = await videoHasActiveScheduleWithType(client, aid, type); const hashActiveSchedule = await videoHasActiveScheduleWithType(sql, aid, type);
if (type == "milestone" && hashActiveSchedule) { if (type == "milestone" && hashActiveSchedule) {
const latestActiveSchedule = await getLatestActiveScheduleWithType(client, aid, type); const latestActiveSchedule = await getLatestActiveScheduleWithType(sql, aid, type);
const latestScheduleStartedAt = new Date(parseTimestampFromPsql(latestActiveSchedule.started_at!)); const latestScheduleStartedAt = new Date(parseTimestampFromPsql(latestActiveSchedule.started_at!));
if (latestScheduleStartedAt > adjustedTime) { if (latestScheduleStartedAt > adjustedTime) {
await client.queryObject(` await sql`
UPDATE snapshot_schedule UPDATE snapshot_schedule
SET started_at = $1 SET started_at = ${adjustedTime}
WHERE id = $2 WHERE id = ${latestActiveSchedule.id}
`, [adjustedTime, latestActiveSchedule.id]); `;
logger.log( logger.log(
`Updated snapshot schedule for ${aid} at ${adjustedTime.toISOString()}`, `Updated snapshot schedule for ${aid} at ${adjustedTime.toISOString()}`,
"mq", "mq",
"fn:scheduleSnapshot", "fn:scheduleSnapshot"
); );
return; return;
} }
@ -220,28 +220,33 @@ export async function scheduleSnapshot(
adjustedTime = await adjustSnapshotTime(new Date(targetTime), 2000, redis); adjustedTime = await adjustSnapshotTime(new Date(targetTime), 2000, redis);
} }
logger.log(`Scheduled snapshot for ${aid} at ${adjustedTime.toISOString()}`, "mq", "fn:scheduleSnapshot"); logger.log(`Scheduled snapshot for ${aid} at ${adjustedTime.toISOString()}`, "mq", "fn:scheduleSnapshot");
return client.queryObject( return sql`
`INSERT INTO snapshot_schedule (aid, type, started_at) VALUES ($1, $2, $3)`, INSERT INTO snapshot_schedule
[aid, type, adjustedTime.toISOString()], (aid, type, started_at)
); VALUES (
${aid},
${type},
${adjustedTime.toISOString()}
)
`;
} }
export async function bulkScheduleSnapshot( export async function bulkScheduleSnapshot(
client: Client, sql: Psql,
aids: number[], aids: number[],
type: string, type: string,
targetTime: number, targetTime: number,
force: boolean = false, force: boolean = false
) { ) {
for (const aid of aids) { for (const aid of aids) {
await scheduleSnapshot(client, aid, type, targetTime, force); await scheduleSnapshot(sql, aid, type, targetTime, force);
} }
} }
export async function adjustSnapshotTime( export async function adjustSnapshotTime(
expectedStartTime: Date, expectedStartTime: Date,
allowedCounts: number = 1000, allowedCounts: number = 1000,
redisClient: Redis, redisClient: Redis
): Promise<Date> { ): Promise<Date> {
const currentWindow = getCurrentWindowIndex(); const currentWindow = getCurrentWindowIndex();
const targetOffset = Math.floor((expectedStartTime.getTime() - Date.now()) / (5 * MINUTE)) - 6; const targetOffset = Math.floor((expectedStartTime.getTime() - Date.now()) / (5 * MINUTE)) - 6;
@ -286,8 +291,8 @@ export async function adjustSnapshotTime(
return expectedStartTime; return expectedStartTime;
} }
export async function getSnapshotsInNextSecond(client: Client) { export async function getSnapshotsInNextSecond(sql: Psql) {
const query = ` const rows = await sql<SnapshotScheduleType[]>`
SELECT * SELECT *
FROM snapshot_schedule FROM snapshot_schedule
WHERE started_at <= NOW() + INTERVAL '1 seconds' AND status = 'pending' AND type != 'normal' WHERE started_at <= NOW() + INTERVAL '1 seconds' AND status = 'pending' AND type != 'normal'
@ -298,13 +303,12 @@ export async function getSnapshotsInNextSecond(client: Client) {
END, END,
started_at started_at
LIMIT 10; LIMIT 10;
`; `
const res = await client.queryObject<SnapshotScheduleType>(query, []); return rows;
return res.rows;
} }
export async function getBulkSnapshotsInNextSecond(client: Client) { export async function getBulkSnapshotsInNextSecond(sql: Psql) {
const query = ` const rows = await sql<SnapshotScheduleType[]>`
SELECT * SELECT *
FROM snapshot_schedule FROM snapshot_schedule
WHERE (started_at <= NOW() + INTERVAL '15 seconds') WHERE (started_at <= NOW() + INTERVAL '15 seconds')
@ -316,43 +320,38 @@ export async function getBulkSnapshotsInNextSecond(client: Client) {
END, END,
started_at started_at
LIMIT 1000; LIMIT 1000;
`
return rows;
}
export async function setSnapshotStatus(sql: Psql, id: number, status: string) {
return await sql`
UPDATE snapshot_schedule SET status = ${status} WHERE id = ${id}
`; `;
const res = await client.queryObject<SnapshotScheduleType>(query, []);
return res.rows;
} }
export async function setSnapshotStatus(client: Client, id: number, status: string) { export async function bulkSetSnapshotStatus(sql: Psql, ids: number[], status: string) {
return await client.queryObject( return await sql`
`UPDATE snapshot_schedule SET status = $2 WHERE id = $1`, UPDATE snapshot_schedule SET status = ${status} WHERE id = ANY(${ids})
[id, status], `;
);
} }
export async function bulkSetSnapshotStatus(client: Client, ids: number[], status: string) { export async function getVideosWithoutActiveSnapshotSchedule(sql: Psql) {
return await client.queryObject( const rows = await sql<{ aid: string }[]>`
`UPDATE snapshot_schedule SET status = $2 WHERE id = ANY($1)`,
[ids, status],
);
}
export async function getVideosWithoutActiveSnapshotSchedule(client: Client) {
const query: string = `
SELECT s.aid SELECT s.aid
FROM songs s FROM songs s
LEFT JOIN snapshot_schedule ss ON s.aid = ss.aid AND (ss.status = 'pending' OR ss.status = 'processing') LEFT JOIN snapshot_schedule ss ON s.aid = ss.aid AND (ss.status = 'pending' OR ss.status = 'processing')
WHERE ss.aid IS NULL WHERE ss.aid IS NULL
`; `;
const res = await client.queryObject<{ aid: number }>(query, []); return rows.map((r) => Number(r.aid));
return res.rows.map((r) => Number(r.aid));
} }
export async function getAllVideosWithoutActiveSnapshotSchedule(client: Client) { export async function getAllVideosWithoutActiveSnapshotSchedule(psql: Psql) {
const query: string = ` const rows = await psql<{ aid: number }[]>`
SELECT s.aid SELECT s.aid
FROM bilibili_metadata s FROM bilibili_metadata s
LEFT JOIN snapshot_schedule ss ON s.aid = ss.aid AND (ss.status = 'pending' OR ss.status = 'processing') LEFT JOIN snapshot_schedule ss ON s.aid = ss.aid AND (ss.status = 'pending' OR ss.status = 'processing')
WHERE ss.aid IS NULL WHERE ss.aid IS NULL
`; `
const res = await client.queryObject<{ aid: number }>(query, []); return rows.map((r) => Number(r.aid));
return res.rows.map((r) => Number(r.aid));
} }

View File

@ -1,8 +1,8 @@
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts"; import type { Psql } from "global.d.ts";
import { parseTimestampFromPsql } from "utils/formatTimestampToPostgre.ts"; import { parseTimestampFromPsql } from "utils/formatTimestampToPostgre.ts";
export async function getNotCollectedSongs(client: Client) { export async function getNotCollectedSongs(sql: Psql) {
const queryResult = await client.queryObject<{ aid: number }>(` const rows = await sql<{ aid: number }[]>`
SELECT lr.aid SELECT lr.aid
FROM labelling_result lr FROM labelling_result lr
WHERE lr.label != 0 WHERE lr.label != 0
@ -11,35 +11,29 @@ export async function getNotCollectedSongs(client: Client) {
FROM songs s FROM songs s
WHERE s.aid = lr.aid WHERE s.aid = lr.aid
); );
`); `;
return queryResult.rows.map((row) => row.aid); return rows.map((row) => row.aid);
} }
export async function aidExistsInSongs(client: Client, aid: number) { export async function aidExistsInSongs(sql: Psql, aid: number) {
const queryResult = await client.queryObject<{ exists: boolean }>( const rows = await sql<{ exists: boolean }[]>`
`
SELECT EXISTS ( SELECT EXISTS (
SELECT 1 SELECT 1
FROM songs FROM songs
WHERE aid = $1 WHERE aid = ${aid}
); );
`, `;
[aid], return rows[0].exists;
);
return queryResult.rows[0].exists;
} }
export async function getSongsPublihsedAt(client: Client, aid: number) { export async function getSongsPublihsedAt(sql: Psql, aid: number) {
const queryResult = await client.queryObject<{ published_at: string }>( const rows = await sql<{ published_at: string }[]>`
`
SELECT published_at SELECT published_at
FROM songs FROM songs
WHERE aid = $1; WHERE aid = ${aid};
`, `;
[aid], if (rows.length === 0) {
);
if (queryResult.rows.length === 0) {
return null; return null;
} }
return parseTimestampFromPsql(queryResult.rows[0].published_at); return parseTimestampFromPsql(rows[0].published_at);
} }

View File

@ -1,32 +0,0 @@
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
import { db } from "db/init.ts";
/**
* Executes a function with a database connection.
* @param operation The function that accepts the `client` as the parameter.
* @param errorHandling Optional function to handle errors.
* If no error handling function is provided, the error will be re-thrown.
* @param cleanup Optional function to execute after the operation.
* @returns The result of the operation or undefined if an error occurred.
*/
export async function withDbConnection<T>(
operation: (client: Client) => Promise<T>,
errorHandling?: (error: unknown, client: Client) => void,
cleanup?: () => void,
): Promise<T | undefined> {
const client = await db.connect();
try {
return await operation(client);
} catch (error) {
if (errorHandling) {
errorHandling(error, client);
return;
}
throw error;
} finally {
client.release();
if (cleanup) {
cleanup();
}
}
}

View File

@ -1,47 +0,0 @@
{
"name": "@cvsa/crawler",
"tasks": {
"crawl-raw-bili": "deno --allow-env --allow-ffi --allow-read --allow-net --allow-write --allow-run src/db/raw/insertAidsToDB.ts",
"crawl-bili-aids": "deno --allow-env --allow-ffi --allow-read --allow-net --allow-write --allow-run src/db/raw/fetchAids.ts",
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
"manifest": "deno task cli manifest $(pwd)",
"start": "deno run -A --watch=static/,routes/ dev.ts",
"build": "deno run -A dev.ts build",
"preview": "deno run -A main.ts",
"worker:main": "deno run --env-file=.env --allow-env --allow-read --allow-ffi --allow-net --allow-write --allow-run ./src/worker.ts",
"worker:filter": "deno run --env-file=.env --allow-env --allow-read --allow-ffi --allow-net --allow-write ./src/filterWorker.ts",
"adder": "deno run --env-file=.env --allow-env --allow-read --allow-ffi --allow-net ./src/jobAdder.ts",
"bullui": "deno run --allow-read --allow-env --allow-ffi --allow-net ./src/bullui.ts",
"all": "concurrently --restart-tries -1 'deno task worker:main' 'deno task adder' 'deno task bullui' 'deno task worker:filter'",
"test": "deno test ./test/ --allow-env --allow-ffi --allow-read --allow-net --allow-write --allow-run"
},
"lint": {
"rules": {
"tags": ["recommended"]
}
},
"imports": {
"@std/assert": "jsr:@std/assert@1",
"$std/": "https://deno.land/std@0.216.0/",
"@std/datetime": "jsr:@std/datetime@^0.225.4",
"@huggingface/transformers": "npm:@huggingface/transformers@3.0.0",
"bullmq": "npm:bullmq",
"mq/": "./mq/",
"db/": "./db/",
"@core/": "../core/",
"log/": "../core/log/",
"net/": "./net/",
"ml/": "./ml/",
"utils/": "./utils/",
"ioredis": "npm:ioredis",
"@bull-board/api": "npm:@bull-board/api",
"@bull-board/express": "npm:@bull-board/express",
"express": "npm:express",
"src/": "./src/",
"onnxruntime": "npm:onnxruntime-node@1.19.2",
"chalk": "npm:chalk",
"@core/db/schema": "../core/db/schema.d.ts",
"@core/db/pgConfig": "../core/db/pgConfig.ts"
},
"exports": "./main.ts"
}

3
packages/crawler/global.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
import type postgres from "postgres";
export type Psql = postgres.Sql<{}>;

View File

@ -1,16 +1,17 @@
import { AIManager } from "ml/manager.ts"; import { AIManager } from "ml/manager.ts";
import * as ort from "onnxruntime"; import * as ort from "onnxruntime-node";
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
import { WorkerError } from "mq/schema.ts"; import { WorkerError } from "mq/schema.ts";
import { AutoTokenizer, PreTrainedTokenizer } from "@huggingface/transformers"; import { AutoTokenizer, PreTrainedTokenizer } from "@huggingface/transformers";
import { AkariModelVersion } from "./const";
const tokenizerModel = "alikia2x/jina-embedding-v3-m2v-1024"; const tokenizerModel = "alikia2x/jina-embedding-v3-m2v-1024";
const onnxClassifierPath = "../../model/akari/3.17.onnx"; const onnxClassifierPath = `../../model/akari/${AkariModelVersion}.onnx`;
const onnxEmbeddingPath = "../../model/embedding/model.onnx"; const onnxEmbeddingPath = "../../model/embedding/model.onnx";
class AkariProto extends AIManager { class AkariProto extends AIManager {
private tokenizer: PreTrainedTokenizer | null = null; private tokenizer: PreTrainedTokenizer | null = null;
private readonly modelVersion = "3.17"; private readonly modelVersion = AkariModelVersion;
constructor() { constructor() {
super(); super();
@ -104,4 +105,5 @@ class AkariProto extends AIManager {
} }
const Akari = new AkariProto(); const Akari = new AkariProto();
await Akari.init();
export default Akari; export default Akari;

View File

@ -0,0 +1 @@
export const AkariModelVersion = "3.17";

View File

@ -1,5 +1,5 @@
import * as ort from "onnxruntime"; import * as ort from "onnxruntime-node";
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
import { WorkerError } from "mq/schema.ts"; import { WorkerError } from "mq/schema.ts";
export class AIManager { export class AIManager {

View File

@ -1,40 +1,40 @@
import { Job } from "npm:bullmq@5.45.2"; import { Job } from "bullmq";
import { getAllVideosWithoutActiveSnapshotSchedule, scheduleSnapshot } from "db/snapshotSchedule.ts"; import { getAllVideosWithoutActiveSnapshotSchedule, scheduleSnapshot } from "db/snapshotSchedule.ts";
import { withDbConnection } from "db/withConnection.ts"; import logger from "@core/log/logger.ts";
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
import logger from "log/logger.ts";
import { lockManager } from "mq/lockManager.ts"; import { lockManager } from "mq/lockManager.ts";
import { getLatestVideoSnapshot } from "db/snapshot.ts"; import { getLatestVideoSnapshot } from "db/snapshot.ts";
import { HOUR, MINUTE } from "$std/datetime/constants.ts"; import { HOUR, MINUTE } from "@core/const/time.ts";
import { sql } from "@core/db/dbNew";
export const archiveSnapshotsWorker = async (_job: Job) => export const archiveSnapshotsWorker = async (_job: Job) => {
await withDbConnection<void>(async (client: Client) => { try {
const startedAt = Date.now(); const startedAt = Date.now();
if (await lockManager.isLocked("dispatchArchiveSnapshots")) { if (await lockManager.isLocked("dispatchArchiveSnapshots")) {
logger.log("dispatchArchiveSnapshots is already running", "mq"); logger.log("dispatchArchiveSnapshots is already running", "mq");
return; return;
} }
await lockManager.acquireLock("dispatchArchiveSnapshots", 30 * 60); await lockManager.acquireLock("dispatchArchiveSnapshots", 30 * 60);
const aids = await getAllVideosWithoutActiveSnapshotSchedule(client); const aids = await getAllVideosWithoutActiveSnapshotSchedule(sql);
for (const rawAid of aids) { for (const rawAid of aids) {
const aid = Number(rawAid); const aid = Number(rawAid);
const latestSnapshot = await getLatestVideoSnapshot(client, aid); const latestSnapshot = await getLatestVideoSnapshot(sql, aid);
const now = Date.now(); const now = Date.now();
const lastSnapshotedAt = latestSnapshot?.time ?? now; const lastSnapshotedAt = latestSnapshot?.time ?? now;
const interval = 168; const interval = 168;
logger.log( logger.log(
`Scheduled archive snapshot for aid ${aid} in ${interval} hours.`, `Scheduled archive snapshot for aid ${aid} in ${interval} hours.`,
"mq", "mq",
"fn:archiveSnapshotsWorker", "fn:archiveSnapshotsWorker"
); );
const targetTime = lastSnapshotedAt + interval * HOUR; const targetTime = lastSnapshotedAt + interval * HOUR;
await scheduleSnapshot(client, aid, "archive", targetTime); await scheduleSnapshot(sql, aid, "archive", targetTime);
if (now - startedAt > 250 * MINUTE) { if (now - startedAt > 250 * MINUTE) {
return; return;
} }
} }
}, (e) => { } catch (e) {
logger.error(e as Error, "mq", "fn:archiveSnapshotsWorker"); logger.error(e as Error, "mq", "fn:archiveSnapshotsWorker");
}, async () => { } finally {
await lockManager.releaseLock("dispatchArchiveSnapshots"); await lockManager.releaseLock("dispatchArchiveSnapshots");
}); }
};

View File

@ -1,23 +1,22 @@
import { Job } from "bullmq"; import { Job } from "bullmq";
import { db } from "db/init.ts";
import { getUnlabelledVideos, getVideoInfoFromAllData, insertVideoLabel } from "../../db/bilibili_metadata.ts"; import { getUnlabelledVideos, getVideoInfoFromAllData, insertVideoLabel } from "../../db/bilibili_metadata.ts";
import Akari from "ml/akari.ts"; import Akari from "ml/akari.ts";
import { ClassifyVideoQueue } from "mq/index.ts"; import { ClassifyVideoQueue } from "mq/index.ts";
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
import { lockManager } from "mq/lockManager.ts"; import { lockManager } from "mq/lockManager.ts";
import { aidExistsInSongs } from "db/songs.ts"; import { aidExistsInSongs } from "db/songs.ts";
import { insertIntoSongs } from "mq/task/collectSongs.ts"; import { insertIntoSongs } from "mq/task/collectSongs.ts";
import { scheduleSnapshot } from "db/snapshotSchedule.ts"; import { scheduleSnapshot } from "db/snapshotSchedule.ts";
import { MINUTE } from "@std/datetime"; import { MINUTE } from "@core/const/time.ts";
import { sql } from "@core/db/dbNew.ts";
export const classifyVideoWorker = async (job: Job) => { export const classifyVideoWorker = async (job: Job) => {
const client = await db.connect();
const aid = job.data.aid; const aid = job.data.aid;
if (!aid) { if (!aid) {
return 3; return 3;
} }
const videoInfo = await getVideoInfoFromAllData(client, aid); const videoInfo = await getVideoInfoFromAllData(sql, aid);
const title = videoInfo.title?.trim() || "untitled"; const title = videoInfo.title?.trim() || "untitled";
const description = videoInfo.description?.trim() || "N/A"; const description = videoInfo.description?.trim() || "N/A";
const tags = videoInfo.tags?.trim() || "empty"; const tags = videoInfo.tags?.trim() || "empty";
@ -25,16 +24,14 @@ export const classifyVideoWorker = async (job: Job) => {
if (label == -1) { if (label == -1) {
logger.warn(`Failed to classify video ${aid}`, "ml"); logger.warn(`Failed to classify video ${aid}`, "ml");
} }
await insertVideoLabel(client, aid, label); await insertVideoLabel(sql, aid, label);
const exists = await aidExistsInSongs(client, aid); const exists = await aidExistsInSongs(sql, aid);
if (!exists && label !== 0) { if (!exists && label !== 0) {
await scheduleSnapshot(client, aid, "new", Date.now() + 10 * MINUTE, true); await scheduleSnapshot(sql, aid, "new", Date.now() + 10 * MINUTE, true);
await insertIntoSongs(client, aid); await insertIntoSongs(sql, aid);
} }
client.release();
await job.updateData({ await job.updateData({
...job.data, ...job.data,
label: label, label: label,
@ -51,10 +48,8 @@ export const classifyVideosWorker = async () => {
await lockManager.acquireLock("classifyVideos"); await lockManager.acquireLock("classifyVideos");
const client = await db.connect(); const videos = await getUnlabelledVideos(sql);
const videos = await getUnlabelledVideos(client);
logger.log(`Found ${videos.length} unlabelled videos`); logger.log(`Found ${videos.length} unlabelled videos`);
client.release();
let i = 0; let i = 0;
for (const aid of videos) { for (const aid of videos) {

View File

@ -1,9 +1,7 @@
import { Job } from "npm:bullmq@5.45.2"; import { Job } from "bullmq";
import { collectSongs } from "mq/task/collectSongs.ts"; import { collectSongs } from "mq/task/collectSongs.ts";
import { withDbConnection } from "db/withConnection.ts";
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
export const collectSongsWorker = (_job: Job): Promise<void> => export const collectSongsWorker = async (_job: Job): Promise<void> =>{
withDbConnection(async (client: Client) => { await collectSongs();
await collectSongs(client); return;
}); }

View File

@ -1,19 +1,18 @@
import { Job } from "npm:bullmq@5.45.2"; import { Job } from "bullmq";
import { withDbConnection } from "db/withConnection.ts";
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
import { getVideosNearMilestone } from "db/snapshot.ts"; import { getVideosNearMilestone } from "db/snapshot.ts";
import { getAdjustedShortTermETA } from "mq/scheduling.ts"; import { getAdjustedShortTermETA } from "mq/scheduling.ts";
import { truncate } from "utils/truncate.ts"; import { truncate } from "utils/truncate.ts";
import { scheduleSnapshot } from "db/snapshotSchedule.ts"; import { scheduleSnapshot } from "db/snapshotSchedule.ts";
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
import { HOUR, MINUTE, SECOND } from "@std/datetime"; import { HOUR, MINUTE, SECOND } from "@core/const/time.ts";
import { sql } from "@core/db/dbNew";
export const dispatchMilestoneSnapshotsWorker = (_job: Job): Promise<void> => export const dispatchMilestoneSnapshotsWorker = async (_job: Job) => {
withDbConnection(async (client: Client) => { try {
const videos = await getVideosNearMilestone(client); const videos = await getVideosNearMilestone(sql);
for (const video of videos) { for (const video of videos) {
const aid = Number(video.aid); const aid = Number(video.aid);
const eta = await getAdjustedShortTermETA(client, aid); const eta = await getAdjustedShortTermETA(sql, aid);
if (eta > 144) continue; if (eta > 144) continue;
const now = Date.now(); const now = Date.now();
const scheduledNextSnapshotDelay = eta * HOUR; const scheduledNextSnapshotDelay = eta * HOUR;
@ -21,9 +20,10 @@ export const dispatchMilestoneSnapshotsWorker = (_job: Job): Promise<void> =>
const minInterval = 1 * SECOND; const minInterval = 1 * SECOND;
const delay = truncate(scheduledNextSnapshotDelay, minInterval, maxInterval); const delay = truncate(scheduledNextSnapshotDelay, minInterval, maxInterval);
const targetTime = now + delay; const targetTime = now + delay;
await scheduleSnapshot(client, aid, "milestone", targetTime); await scheduleSnapshot(sql, aid, "milestone", targetTime);
logger.log(`Scheduled milestone snapshot for aid ${aid} in ${(delay / MINUTE).toFixed(2)} mins.`, "mq"); logger.log(`Scheduled milestone snapshot for aid ${aid} in ${(delay / MINUTE).toFixed(2)} mins.`, "mq");
} }
}, (e) => { } catch (e) {
logger.error(e as Error, "mq", "fn:dispatchMilestoneSnapshotsWorker"); logger.error(e as Error, "mq", "fn:dispatchMilestoneSnapshotsWorker");
}); };
}

View File

@ -1,16 +1,15 @@
import { Job } from "npm:bullmq@5.45.2"; import { Job } from "bullmq";
import { withDbConnection } from "db/withConnection.ts";
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
import { getLatestVideoSnapshot } from "db/snapshot.ts"; import { getLatestVideoSnapshot } from "db/snapshot.ts";
import { truncate } from "utils/truncate.ts"; import { truncate } from "utils/truncate.ts";
import { getVideosWithoutActiveSnapshotSchedule, scheduleSnapshot } from "db/snapshotSchedule.ts"; import { getVideosWithoutActiveSnapshotSchedule, scheduleSnapshot } from "db/snapshotSchedule.ts";
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
import { HOUR, MINUTE, WEEK } from "@std/datetime"; import { HOUR, MINUTE, WEEK } from "@core/const/time.ts";
import { lockManager } from "../lockManager.ts"; import { lockManager } from "mq/lockManager.ts";
import { getRegularSnapshotInterval } from "../task/regularSnapshotInterval.ts"; import { getRegularSnapshotInterval } from "mq/task/regularSnapshotInterval.ts";
import { sql } from "@core/db/dbNew.ts";
export const dispatchRegularSnapshotsWorker = async (_job: Job): Promise<void> => export const dispatchRegularSnapshotsWorker = async (_job: Job): Promise<void> => {
await withDbConnection(async (client: Client) => { try {
const startedAt = Date.now(); const startedAt = Date.now();
if (await lockManager.isLocked("dispatchRegularSnapshots")) { if (await lockManager.isLocked("dispatchRegularSnapshots")) {
logger.log("dispatchRegularSnapshots is already running", "mq"); logger.log("dispatchRegularSnapshots is already running", "mq");
@ -18,22 +17,23 @@ export const dispatchRegularSnapshotsWorker = async (_job: Job): Promise<void> =
} }
await lockManager.acquireLock("dispatchRegularSnapshots", 30 * 60); await lockManager.acquireLock("dispatchRegularSnapshots", 30 * 60);
const aids = await getVideosWithoutActiveSnapshotSchedule(client); const aids = await getVideosWithoutActiveSnapshotSchedule(sql);
for (const rawAid of aids) { for (const rawAid of aids) {
const aid = Number(rawAid); const aid = Number(rawAid);
const latestSnapshot = await getLatestVideoSnapshot(client, aid); const latestSnapshot = await getLatestVideoSnapshot(sql, aid);
const now = Date.now(); const now = Date.now();
const lastSnapshotedAt = latestSnapshot?.time ?? now; const lastSnapshotedAt = latestSnapshot?.time ?? now;
const interval = await getRegularSnapshotInterval(client, aid); const interval = await getRegularSnapshotInterval(sql, aid);
logger.log(`Scheduled regular snapshot for aid ${aid} in ${interval} hours.`, "mq"); logger.log(`Scheduled regular snapshot for aid ${aid} in ${interval} hours.`, "mq");
const targetTime = truncate(lastSnapshotedAt + interval * HOUR, now + 1, now + 100000 * WEEK); const targetTime = truncate(lastSnapshotedAt + interval * HOUR, now + 1, now + 100000 * WEEK);
await scheduleSnapshot(client, aid, "normal", targetTime); await scheduleSnapshot(sql, aid, "normal", targetTime);
if (now - startedAt > 25 * MINUTE) { if (now - startedAt > 25 * MINUTE) {
return; return;
} }
} }
}, (e) => { } catch (e) {
logger.error(e as Error, "mq", "fn:regularSnapshotsWorker"); logger.error(e as Error, "mq", "fn:regularSnapshotsWorker");
}, async () => { } finally {
await lockManager.releaseLock("dispatchRegularSnapshots"); await lockManager.releaseLock("dispatchRegularSnapshots");
}); }
};

View File

@ -1,9 +1,7 @@
import { sql } from "@core/db/dbNew";
import { Job } from "bullmq"; import { Job } from "bullmq";
import { queueLatestVideos } from "mq/task/queueLatestVideo.ts"; import { queueLatestVideos } from "mq/task/queueLatestVideo.ts";
import { withDbConnection } from "db/withConnection.ts";
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
export const getLatestVideosWorker = (_job: Job): Promise<void> => export const getLatestVideosWorker = async (_job: Job): Promise<void> =>{
withDbConnection(async (client: Client) => { await queueLatestVideos(sql);
await queueLatestVideos(client); }
});

View File

@ -1,15 +1,13 @@
import { Job } from "npm:bullmq@5.45.2"; import { Job } from "bullmq";
import { insertVideoInfo } from "mq/task/getVideoDetails.ts"; import { insertVideoInfo } from "mq/task/getVideoDetails.ts";
import { withDbConnection } from "db/withConnection.ts"; import logger from "@core/log/logger.ts";
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts"; import { sql } from "@core/db/dbNew";
import logger from "log/logger.ts";
export const getVideoInfoWorker = async (job: Job): Promise<void> => export const getVideoInfoWorker = async (job: Job): Promise<void> => {
await withDbConnection<void>(async (client: Client) => {
const aid = job.data.aid; const aid = job.data.aid;
if (!aid) { if (!aid) {
logger.warn("aid does not exists", "mq", "job:getVideoInfo"); logger.warn("aid does not exists", "mq", "job:getVideoInfo");
return; return;
} }
await insertVideoInfo(client, aid); await insertVideoInfo(sql, aid);
}); }

View File

@ -1,20 +1,25 @@
import { Job } from "npm:bullmq@5.45.2"; import { Job } from "bullmq";
import { withDbConnection } from "db/withConnection.ts"; import { sql } from "@core/db/dbNew";
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts"; import logger from "@core/log/logger.ts";
import logger from "log/logger.ts";
import { scheduleSnapshot, setSnapshotStatus } from "db/snapshotSchedule.ts"; import { scheduleSnapshot, setSnapshotStatus } from "db/snapshotSchedule.ts";
import { SECOND } from "@std/datetime"; import { SECOND } from "@core/const/time.ts";
import { getTimeoutSchedulesCount } from "mq/task/getTimeoutSchedulesCount.ts"; import { getTimeoutSchedulesCount } from "mq/task/getTimeoutSchedulesCount.ts";
import { removeAllTimeoutSchedules } from "mq/task/removeAllTimeoutSchedules.ts"; import { removeAllTimeoutSchedules } from "mq/task/removeAllTimeoutSchedules.ts";
export const scheduleCleanupWorker = async (_job: Job): Promise<void> => interface SnapshotSchedule {
await withDbConnection<void>(async (client: Client) => { id: bigint;
if (await getTimeoutSchedulesCount(client) > 2000) { aid: bigint;
await removeAllTimeoutSchedules(client); type: string;
}
export const scheduleCleanupWorker = async (_job: Job): Promise<void> => {
try {
if ((await getTimeoutSchedulesCount()) > 2000) {
await removeAllTimeoutSchedules();
return; return;
} }
const query: string = ` const rows = await sql<SnapshotSchedule[]>`
SELECT id, aid, type SELECT id, aid, type
FROM snapshot_schedule FROM snapshot_schedule
WHERE status IN ('pending', 'processing') WHERE status IN ('pending', 'processing')
@ -26,20 +31,21 @@ export const scheduleCleanupWorker = async (_job: Job): Promise<void> =>
AND started_at < NOW() - INTERVAL '2 minutes' AND started_at < NOW() - INTERVAL '2 minutes'
AND type = 'milestone' AND type = 'milestone'
`; `;
const { rows } = await client.queryObject<{ id: bigint; aid: bigint; type: string }>(query);
if (rows.length === 0) return; if (rows.length === 0) return;
for (const row of rows) { for (const row of rows) {
const id = Number(row.id); const id = Number(row.id);
const aid = Number(row.aid); const aid = Number(row.aid);
const type = row.type; const type = row.type;
await setSnapshotStatus(client, id, "timeout"); await setSnapshotStatus(sql, id, "timeout");
await scheduleSnapshot(client, aid, type, Date.now() + 10 * SECOND); await scheduleSnapshot(sql, aid, type, Date.now() + 10 * SECOND);
logger.log( logger.log(
`Schedule ${id} has not received any response in a while, rescheduled.`, `Schedule ${id} has not received any response in a while, rescheduled.`,
"mq", "mq",
"fn:scheduleCleanupWorker", "fn:scheduleCleanupWorker"
); );
} }
}, (e) => { } catch (e) {
logger.error(e as Error, "mq", "fn:scheduleCleanupWorker"); logger.error(e as Error, "mq", "fn:scheduleCleanupWorker");
}); }
};

View File

@ -1,5 +1,4 @@
import { Job } from "bullmq"; import { Job } from "bullmq";
import { db } from "db/init.ts";
import { import {
bulkGetVideosWithoutProcessingSchedules, bulkGetVideosWithoutProcessingSchedules,
bulkSetSnapshotStatus, bulkSetSnapshotStatus,
@ -8,8 +7,9 @@ import {
setSnapshotStatus, setSnapshotStatus,
videoHasProcessingSchedule, videoHasProcessingSchedule,
} from "db/snapshotSchedule.ts"; } from "db/snapshotSchedule.ts";
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
import { SnapshotQueue } from "mq/index.ts"; import { SnapshotQueue } from "mq/index.ts";
import { sql } from "@core/db/dbNew";
const priorityMap: { [key: string]: number } = { const priorityMap: { [key: string]: number } = {
"milestone": 1, "milestone": 1,
@ -17,17 +17,16 @@ const priorityMap: { [key: string]: number } = {
}; };
export const bulkSnapshotTickWorker = async (_job: Job) => { export const bulkSnapshotTickWorker = async (_job: Job) => {
const client = await db.connect();
try { try {
const schedules = await getBulkSnapshotsInNextSecond(client); const schedules = await getBulkSnapshotsInNextSecond(sql);
const count = schedules.length; const count = schedules.length;
const groups = Math.ceil(count / 30); const groups = Math.ceil(count / 30);
for (let i = 0; i < groups; i++) { for (let i = 0; i < groups; i++) {
const group = schedules.slice(i * 30, (i + 1) * 30); const group = schedules.slice(i * 30, (i + 1) * 30);
const aids = group.map((schedule) => Number(schedule.aid)); const aids = group.map((schedule) => Number(schedule.aid));
const filteredAids = await bulkGetVideosWithoutProcessingSchedules(client, aids); const filteredAids = await bulkGetVideosWithoutProcessingSchedules(sql, aids);
if (filteredAids.length === 0) continue; if (filteredAids.length === 0) continue;
await bulkSetSnapshotStatus(client, filteredAids, "processing"); await bulkSetSnapshotStatus(sql, filteredAids, "processing");
const schedulesData = group.map((schedule) => { const schedulesData = group.map((schedule) => {
return { return {
aid: Number(schedule.aid), aid: Number(schedule.aid),
@ -46,17 +45,14 @@ export const bulkSnapshotTickWorker = async (_job: Job) => {
return `OK`; return `OK`;
} catch (e) { } catch (e) {
logger.error(e as Error); logger.error(e as Error);
} finally {
client.release();
} }
}; };
export const snapshotTickWorker = async (_job: Job) => { export const snapshotTickWorker = async (_job: Job) => {
const client = await db.connect();
try { try {
const schedules = await getSnapshotsInNextSecond(client); const schedules = await getSnapshotsInNextSecond(sql);
for (const schedule of schedules) { for (const schedule of schedules) {
if (await videoHasProcessingSchedule(client, Number(schedule.aid))) { if (await videoHasProcessingSchedule(sql, Number(schedule.aid))) {
continue; continue;
} }
let priority = 3; let priority = 3;
@ -64,7 +60,7 @@ export const snapshotTickWorker = async (_job: Job) => {
priority = priorityMap[schedule.type]; priority = priorityMap[schedule.type];
} }
const aid = Number(schedule.aid); const aid = Number(schedule.aid);
await setSnapshotStatus(client, schedule.id, "processing"); await setSnapshotStatus(sql, schedule.id, "processing");
await SnapshotQueue.add("snapshotVideo", { await SnapshotQueue.add("snapshotVideo", {
aid: Number(aid), aid: Number(aid),
id: Number(schedule.id), id: Number(schedule.id),
@ -74,8 +70,6 @@ export const snapshotTickWorker = async (_job: Job) => {
return `OK`; return `OK`;
} catch (e) { } catch (e) {
logger.error(e as Error); logger.error(e as Error);
} finally {
client.release();
} }
}; };

View File

@ -1,15 +1,14 @@
import { Job } from "npm:bullmq@5.45.2"; import { Job } from "bullmq";
import { withDbConnection } from "db/withConnection.ts";
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
import { scheduleSnapshot, setSnapshotStatus, snapshotScheduleExists } from "db/snapshotSchedule.ts"; import { scheduleSnapshot, setSnapshotStatus, snapshotScheduleExists } from "db/snapshotSchedule.ts";
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
import { HOUR, MINUTE, SECOND } from "@std/datetime"; import { HOUR, MINUTE, SECOND } from "@core/const/time.ts";
import { lockManager } from "mq/lockManager.ts"; import { lockManager } from "mq/lockManager.ts";
import { getBiliVideoStatus, setBiliVideoStatus } from "../../db/bilibili_metadata.ts"; import { getBiliVideoStatus, setBiliVideoStatus } from "../../db/bilibili_metadata.ts";
import { insertVideoSnapshot } from "mq/task/getVideoStats.ts"; import { insertVideoSnapshot } from "mq/task/getVideoStats.ts";
import { getSongsPublihsedAt } from "db/songs.ts"; import { getSongsPublihsedAt } from "db/songs.ts";
import { getAdjustedShortTermETA } from "mq/scheduling.ts"; import { getAdjustedShortTermETA } from "mq/scheduling.ts";
import { NetSchedulerError } from "@core/net/delegate.ts"; import { NetSchedulerError } from "@core/net/delegate.ts";
import { sql } from "@core/db/dbNew.ts";
const snapshotTypeToTaskMap: { [key: string]: string } = { const snapshotTypeToTaskMap: { [key: string]: string } = {
"milestone": "snapshotMilestoneVideo", "milestone": "snapshotMilestoneVideo",
@ -23,12 +22,12 @@ export const snapshotVideoWorker = async (job: Job): Promise<void> => {
const type = job.data.type; const type = job.data.type;
const task = snapshotTypeToTaskMap[type] ?? "snapshotVideo"; const task = snapshotTypeToTaskMap[type] ?? "snapshotVideo";
const retryInterval = type === "milestone" ? 5 * SECOND : 2 * MINUTE; const retryInterval = type === "milestone" ? 5 * SECOND : 2 * MINUTE;
await withDbConnection(async (client: Client) => { try {
const exists = await snapshotScheduleExists(client, id); const exists = await snapshotScheduleExists(sql, id);
if (!exists) { if (!exists) {
return; return;
} }
const status = await getBiliVideoStatus(client, aid); const status = await getBiliVideoStatus(sql, aid);
if (status !== 0) { if (status !== 0) {
logger.warn( logger.warn(
`Video ${aid} has status ${status} in the database. Abort snapshoting.`, `Video ${aid} has status ${status} in the database. Abort snapshoting.`,
@ -37,11 +36,11 @@ export const snapshotVideoWorker = async (job: Job): Promise<void> => {
); );
return; return;
} }
await setSnapshotStatus(client, id, "processing"); await setSnapshotStatus(sql, id, "processing");
const stat = await insertVideoSnapshot(client, aid, task); const stat = await insertVideoSnapshot(sql, aid, task);
if (typeof stat === "number") { if (typeof stat === "number") {
await setBiliVideoStatus(client, aid, stat); await setBiliVideoStatus(sql, aid, stat);
await setSnapshotStatus(client, id, "bili_error"); await setSnapshotStatus(sql, id, "bili_error");
logger.warn( logger.warn(
`Bilibili return status ${status} when snapshoting for ${aid}.`, `Bilibili return status ${status} when snapshoting for ${aid}.`,
"mq", "mq",
@ -49,9 +48,9 @@ export const snapshotVideoWorker = async (job: Job): Promise<void> => {
); );
return; return;
} }
await setSnapshotStatus(client, id, "completed"); await setSnapshotStatus(sql, id, "completed");
if (type === "new") { if (type === "new") {
const publihsedAt = await getSongsPublihsedAt(client, aid); const publihsedAt = await getSongsPublihsedAt(sql, aid);
const timeSincePublished = stat.time - publihsedAt!; const timeSincePublished = stat.time - publihsedAt!;
const viewsPerHour = stat.views / timeSincePublished * HOUR; const viewsPerHour = stat.views / timeSincePublished * HOUR;
if (timeSincePublished > 48 * HOUR) { if (timeSincePublished > 48 * HOUR) {
@ -70,10 +69,10 @@ export const snapshotVideoWorker = async (job: Job): Promise<void> => {
if (viewsPerHour > 1000) { if (viewsPerHour > 1000) {
intervalMins = 15; intervalMins = 15;
} }
await scheduleSnapshot(client, aid, type, Date.now() + intervalMins * MINUTE, true); await scheduleSnapshot(sql, aid, type, Date.now() + intervalMins * MINUTE, true);
} }
if (type !== "milestone") return; if (type !== "milestone") return;
const eta = await getAdjustedShortTermETA(client, aid); const eta = await getAdjustedShortTermETA(sql, aid);
if (eta > 144) { if (eta > 144) {
const etaHoursString = eta.toFixed(2) + " hrs"; const etaHoursString = eta.toFixed(2) + " hrs";
logger.warn( logger.warn(
@ -84,18 +83,19 @@ export const snapshotVideoWorker = async (job: Job): Promise<void> => {
} }
const now = Date.now(); const now = Date.now();
const targetTime = now + eta * HOUR; const targetTime = now + eta * HOUR;
await scheduleSnapshot(client, aid, type, targetTime); await scheduleSnapshot(sql, aid, type, targetTime);
await setSnapshotStatus(client, id, "completed"); await setSnapshotStatus(sql, id, "completed");
return; return;
}, async (e, client) => { }
catch (e) {
if (e instanceof NetSchedulerError && e.code === "NO_PROXY_AVAILABLE") { if (e instanceof NetSchedulerError && e.code === "NO_PROXY_AVAILABLE") {
logger.warn( logger.warn(
`No available proxy for aid ${job.data.aid}.`, `No available proxy for aid ${job.data.aid}.`,
"mq", "mq",
"fn:takeSnapshotForVideoWorker", "fn:takeSnapshotForVideoWorker",
); );
await setSnapshotStatus(client, id, "no_proxy"); await setSnapshotStatus(sql, id, "no_proxy");
await scheduleSnapshot(client, aid, type, Date.now() + retryInterval); await scheduleSnapshot(sql, aid, type, Date.now() + retryInterval);
return; return;
} }
else if (e instanceof NetSchedulerError && e.code === "ALICLOUD_PROXY_ERR") { else if (e instanceof NetSchedulerError && e.code === "ALICLOUD_PROXY_ERR") {
@ -104,13 +104,14 @@ export const snapshotVideoWorker = async (job: Job): Promise<void> => {
"mq", "mq",
"fn:takeSnapshotForVideoWorker", "fn:takeSnapshotForVideoWorker",
); );
await setSnapshotStatus(client, id, "failed"); await setSnapshotStatus(sql, id, "failed");
await scheduleSnapshot(client, aid, type, Date.now() + retryInterval); await scheduleSnapshot(sql, aid, type, Date.now() + retryInterval);
} }
logger.error(e as Error, "mq", "fn:takeSnapshotForVideoWorker"); logger.error(e as Error, "mq", "fn:takeSnapshotForVideoWorker");
await setSnapshotStatus(client, id, "failed"); await setSnapshotStatus(sql, id, "failed");
}, async () => { }
finally {
await lockManager.releaseLock("dispatchRegularSnapshots"); await lockManager.releaseLock("dispatchRegularSnapshots");
}); };
return; return;
}; };

View File

@ -1,5 +1,4 @@
import { Job } from "npm:bullmq@5.45.2"; import { Job } from "bullmq";
import { db } from "db/init.ts";
import { import {
bulkScheduleSnapshot, bulkScheduleSnapshot,
bulkSetSnapshotStatus, bulkSetSnapshotStatus,
@ -7,22 +6,22 @@ import {
snapshotScheduleExists, snapshotScheduleExists,
} from "db/snapshotSchedule.ts"; } from "db/snapshotSchedule.ts";
import { bulkGetVideoStats } from "net/bulkGetVideoStats.ts"; import { bulkGetVideoStats } from "net/bulkGetVideoStats.ts";
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
import { NetSchedulerError } from "@core/net/delegate.ts"; import { NetSchedulerError } from "@core/net/delegate.ts";
import { HOUR, MINUTE, SECOND } from "@std/datetime"; import { HOUR, MINUTE, SECOND } from "@core/const/time.ts";
import { getRegularSnapshotInterval } from "../task/regularSnapshotInterval.ts"; import { getRegularSnapshotInterval } from "../task/regularSnapshotInterval.ts";
import { SnapshotScheduleType } from "@core/db/schema"; import { SnapshotScheduleType } from "@core/db/schema";
import { sql } from "@core/db/dbNew.ts";
export const takeBulkSnapshotForVideosWorker = async (job: Job) => { export const takeBulkSnapshotForVideosWorker = async (job: Job) => {
const schedules: SnapshotScheduleType[] = job.data.schedules; const schedules: SnapshotScheduleType[] = job.data.schedules;
const ids = schedules.map((schedule) => Number(schedule.id)); const ids = schedules.map((schedule) => Number(schedule.id));
const aidsToFetch: number[] = []; const aidsToFetch: number[] = [];
const client = await db.connect();
try { try {
for (const schedule of schedules) { for (const schedule of schedules) {
const aid = Number(schedule.aid); const aid = Number(schedule.aid);
const id = Number(schedule.id); const id = Number(schedule.id);
const exists = await snapshotScheduleExists(client, id); const exists = await snapshotScheduleExists(sql, id);
if (!exists) { if (!exists) {
continue; continue;
} }
@ -30,8 +29,8 @@ export const takeBulkSnapshotForVideosWorker = async (job: Job) => {
} }
const data = await bulkGetVideoStats(aidsToFetch); const data = await bulkGetVideoStats(aidsToFetch);
if (typeof data === "number") { if (typeof data === "number") {
await bulkSetSnapshotStatus(client, ids, "failed"); await bulkSetSnapshotStatus(sql, ids, "failed");
await bulkScheduleSnapshot(client, aidsToFetch, "normal", Date.now() + 15 * SECOND); await bulkScheduleSnapshot(sql, aidsToFetch, "normal", Date.now() + 15 * SECOND);
return `GET_BILI_STATUS_${data}`; return `GET_BILI_STATUS_${data}`;
} }
for (const video of data) { for (const video of data) {
@ -44,26 +43,31 @@ export const takeBulkSnapshotForVideosWorker = async (job: Job) => {
const coins = stat.coin; const coins = stat.coin;
const shares = stat.share; const shares = stat.share;
const favorites = stat.collect; const favorites = stat.collect;
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},
await client.queryObject( ${views},
query, ${danmakus},
[aid, views, danmakus, replies, likes, coins, shares, favorites], ${replies},
); ${likes},
${coins},
${shares},
${favorites}
)
`
logger.log(`Taken snapshot for video ${aid} in bulk.`, "net", "fn:takeBulkSnapshotForVideosWorker"); logger.log(`Taken snapshot for video ${aid} in bulk.`, "net", "fn:takeBulkSnapshotForVideosWorker");
} }
await bulkSetSnapshotStatus(client, ids, "completed"); await bulkSetSnapshotStatus(sql, ids, "completed");
for (const schedule of schedules) { for (const schedule of schedules) {
const aid = Number(schedule.aid); const aid = Number(schedule.aid);
const type = schedule.type; const type = schedule.type;
if (type == "archive") continue; if (type == "archive") continue;
const interval = await getRegularSnapshotInterval(client, aid); const interval = await getRegularSnapshotInterval(sql, aid);
logger.log(`Scheduled regular snapshot for aid ${aid} in ${interval} hours.`, "mq"); logger.log(`Scheduled regular snapshot for aid ${aid} in ${interval} hours.`, "mq");
await scheduleSnapshot(client, aid, "normal", Date.now() + interval * HOUR); await scheduleSnapshot(sql, aid, "normal", Date.now() + interval * HOUR);
} }
return `DONE`; return `DONE`;
} catch (e) { } catch (e) {
@ -73,13 +77,11 @@ export const takeBulkSnapshotForVideosWorker = async (job: Job) => {
"mq", "mq",
"fn:takeBulkSnapshotForVideosWorker", "fn:takeBulkSnapshotForVideosWorker",
); );
await bulkSetSnapshotStatus(client, ids, "no_proxy"); await bulkSetSnapshotStatus(sql, ids, "no_proxy");
await bulkScheduleSnapshot(client, aidsToFetch, "normal", Date.now() + 20 * MINUTE * Math.random()); await bulkScheduleSnapshot(sql, aidsToFetch, "normal", Date.now() + 20 * MINUTE * Math.random());
return; return;
} }
logger.error(e as Error, "mq", "fn:takeBulkSnapshotForVideosWorker"); logger.error(e as Error, "mq", "fn:takeBulkSnapshotForVideosWorker");
await bulkSetSnapshotStatus(client, ids, "failed"); await bulkSetSnapshotStatus(sql, ids, "failed");
} finally {
client.release();
} }
}; };

View File

@ -1,7 +1,14 @@
import { Queue } from "bullmq"; import { Queue, ConnectionOptions } from "bullmq";
import { redis } from "@core/db/redis.ts";
export const LatestVideosQueue = new Queue("latestVideos"); export const LatestVideosQueue = new Queue("latestVideos", {
connection: redis as ConnectionOptions
});
export const ClassifyVideoQueue = new Queue("classifyVideo"); export const ClassifyVideoQueue = new Queue("classifyVideo", {
connection: redis as ConnectionOptions
});
export const SnapshotQueue = new Queue("snapshot"); export const SnapshotQueue = new Queue("snapshot", {
connection: redis as ConnectionOptions
});

View File

@ -1,72 +1,75 @@
import { HOUR, MINUTE, SECOND } from "$std/datetime/constants.ts"; import { HOUR, MINUTE, SECOND } from "@core/const/time.ts";
import { ClassifyVideoQueue, LatestVideosQueue, SnapshotQueue } from "mq/index.ts"; import { ClassifyVideoQueue, LatestVideosQueue, SnapshotQueue } from "mq/index.ts";
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
import { initSnapshotWindowCounts } from "db/snapshotSchedule.ts"; import { initSnapshotWindowCounts } from "db/snapshotSchedule.ts";
import { db } from "db/init.ts";
import { redis } from "@core/db/redis.ts"; import { redis } from "@core/db/redis.ts";
import { sql } from "@core/db/dbNew";
export async function initMQ() { export async function initMQ() {
const client = await db.connect(); await initSnapshotWindowCounts(sql, redis);
try {
await initSnapshotWindowCounts(client, redis);
await LatestVideosQueue.upsertJobScheduler("getLatestVideos", { await LatestVideosQueue.upsertJobScheduler("getLatestVideos", {
every: 1 * MINUTE, every: 1 * MINUTE,
immediately: true, immediately: true
}); });
await ClassifyVideoQueue.upsertJobScheduler("classifyVideos", { await ClassifyVideoQueue.upsertJobScheduler("classifyVideos", {
every: 5 * MINUTE, every: 5 * MINUTE,
immediately: true, immediately: true
}); });
await LatestVideosQueue.upsertJobScheduler("collectSongs", { await LatestVideosQueue.upsertJobScheduler("collectSongs", {
every: 3 * MINUTE, every: 3 * MINUTE,
immediately: true, immediately: true
}); });
await SnapshotQueue.upsertJobScheduler("snapshotTick", { await SnapshotQueue.upsertJobScheduler(
"snapshotTick",
{
every: 1 * SECOND, every: 1 * SECOND,
immediately: true, immediately: true
}, { },
{
opts: { opts: {
removeOnComplete: 300, removeOnComplete: 300,
removeOnFail: 600, removeOnFail: 600
}, }
}); }
);
await SnapshotQueue.upsertJobScheduler("bulkSnapshotTick", { await SnapshotQueue.upsertJobScheduler(
"bulkSnapshotTick",
{
every: 15 * SECOND, every: 15 * SECOND,
immediately: true, immediately: true
}, { },
{
opts: { opts: {
removeOnComplete: 60, removeOnComplete: 60,
removeOnFail: 600, removeOnFail: 600
}, }
}); }
);
await SnapshotQueue.upsertJobScheduler("dispatchMilestoneSnapshots", { await SnapshotQueue.upsertJobScheduler("dispatchMilestoneSnapshots", {
every: 5 * MINUTE, every: 5 * MINUTE,
immediately: true, immediately: true
}); });
await SnapshotQueue.upsertJobScheduler("dispatchRegularSnapshots", { await SnapshotQueue.upsertJobScheduler("dispatchRegularSnapshots", {
every: 30 * MINUTE, every: 30 * MINUTE,
immediately: true, immediately: true
}); });
await SnapshotQueue.upsertJobScheduler("dispatchArchiveSnapshots", { await SnapshotQueue.upsertJobScheduler("dispatchArchiveSnapshots", {
every: 6 * HOUR, every: 6 * HOUR,
immediately: true, immediately: true
}); });
await SnapshotQueue.upsertJobScheduler("scheduleCleanup", { await SnapshotQueue.upsertJobScheduler("scheduleCleanup", {
every: 2 * MINUTE, every: 2 * MINUTE,
immediately: true, immediately: true
}); });
logger.log("Message queue initialized."); logger.log("Message queue initialized.");
} finally {
client.release();
}
} }

View File

@ -1,8 +1,8 @@
import { findClosestSnapshot, getLatestSnapshot, hasAtLeast2Snapshots } from "db/snapshotSchedule.ts"; import { findClosestSnapshot, getLatestSnapshot, hasAtLeast2Snapshots } from "db/snapshotSchedule.ts";
import { truncate } from "utils/truncate.ts"; import { truncate } from "utils/truncate.ts";
import { closetMilestone } from "./exec/snapshotTick.ts"; import { closetMilestone } from "./exec/snapshotTick.ts";
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts"; import { HOUR, MINUTE } from "@core/const/time.ts";
import { HOUR, MINUTE } from "@std/datetime"; import type { Psql } from "global.d.ts";
const log = (value: number, base: number = 10) => Math.log(value) / Math.log(base); const log = (value: number, base: number = 10) => Math.log(value) / Math.log(base);
@ -26,11 +26,11 @@ const getFactor = (x: number) => {
* @param aid - aid of the video * @param aid - aid of the video
* @returns ETA in hours * @returns ETA in hours
*/ */
export const getAdjustedShortTermETA = async (client: Client, aid: number) => { export const getAdjustedShortTermETA = async (sql: Psql, aid: number) => {
const latestSnapshot = await getLatestSnapshot(client, aid); const latestSnapshot = await getLatestSnapshot(sql, aid);
// Immediately dispatch a snapshot if there is no snapshot yet // Immediately dispatch a snapshot if there is no snapshot yet
if (!latestSnapshot) return 0; if (!latestSnapshot) return 0;
const snapshotsEnough = await hasAtLeast2Snapshots(client, aid); const snapshotsEnough = await hasAtLeast2Snapshots(sql, aid);
if (!snapshotsEnough) return 0; if (!snapshotsEnough) return 0;
const currentTimestamp = new Date().getTime(); const currentTimestamp = new Date().getTime();
@ -40,7 +40,7 @@ export const getAdjustedShortTermETA = async (client: Client, aid: number) => {
for (const timeInterval of timeIntervals) { for (const timeInterval of timeIntervals) {
const date = new Date(currentTimestamp - timeInterval); const date = new Date(currentTimestamp - timeInterval);
const snapshot = await findClosestSnapshot(client, aid, date); const snapshot = await findClosestSnapshot(sql, aid, date);
if (!snapshot) continue; if (!snapshot) continue;
const hoursDiff = (latestSnapshot.created_at - snapshot.created_at) / HOUR; const hoursDiff = (latestSnapshot.created_at - snapshot.created_at) / HOUR;
const viewsDiff = latestSnapshot.views - snapshot.views; const viewsDiff = latestSnapshot.views - snapshot.views;

View File

@ -1,31 +1,29 @@
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts"; import { sql } from "@core/db/dbNew";
import { aidExistsInSongs, getNotCollectedSongs } from "db/songs.ts"; import { aidExistsInSongs, getNotCollectedSongs } from "db/songs.ts";
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
import { scheduleSnapshot } from "db/snapshotSchedule.ts"; import { scheduleSnapshot } from "db/snapshotSchedule.ts";
import { MINUTE } from "@std/datetime"; import { MINUTE } from "@core/const/time.ts";
import type { Psql } from "global.d.ts";
export async function collectSongs(client: Client) { export async function collectSongs() {
const aids = await getNotCollectedSongs(client); const aids = await getNotCollectedSongs(sql);
for (const aid of aids) { for (const aid of aids) {
const exists = await aidExistsInSongs(client, aid); const exists = await aidExistsInSongs(sql, aid);
if (exists) continue; if (exists) continue;
await insertIntoSongs(client, aid); await insertIntoSongs(sql, aid);
await scheduleSnapshot(client, aid, "new", Date.now() + 10 * MINUTE, true); await scheduleSnapshot(sql, aid, "new", Date.now() + 10 * MINUTE, true);
logger.log(`Video ${aid} was added into the songs table.`, "mq", "fn:collectSongs"); logger.log(`Video ${aid} was added into the songs table.`, "mq", "fn:collectSongs");
} }
} }
export async function insertIntoSongs(client: Client, aid: number) { export async function insertIntoSongs(sql: Psql, aid: number) {
await client.queryObject( await sql`
`
INSERT INTO songs (aid, published_at, duration) INSERT INTO songs (aid, published_at, duration)
VALUES ( VALUES (
$1, $1,
(SELECT published_at FROM bilibili_metadata WHERE aid = $1), (SELECT published_at FROM bilibili_metadata WHERE aid = ${aid}),
(SELECT duration FROM bilibili_metadata WHERE aid = $1) (SELECT duration FROM bilibili_metadata WHERE aid = ${aid})
) )
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
`, `
[aid],
);
} }

View File

@ -1,13 +1,11 @@
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts"; import { sql } from "@core/db/dbNew";
export async function getTimeoutSchedulesCount(client: Client) { export async function getTimeoutSchedulesCount() {
const query: string = ` const rows = await sql<{ count: number }[]>`
SELECT COUNT(id) SELECT COUNT(id)
FROM snapshot_schedule FROM snapshot_schedule
WHERE status IN ('pending', 'processing') WHERE status IN ('pending', 'processing')
AND started_at < NOW() - INTERVAL '30 minutes' AND started_at < NOW() - INTERVAL '30 minutes'
`; `;
const { rows } = await client.queryObject<{ count: number }>(query);
return rows[0].count; return rows[0].count;
} }

View File

@ -1,13 +1,13 @@
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
import { getVideoDetails } from "net/getVideoDetails.ts"; import { getVideoDetails } from "net/getVideoDetails.ts";
import { formatTimestampToPsql } from "utils/formatTimestampToPostgre.ts"; import { formatTimestampToPsql } from "utils/formatTimestampToPostgre.ts";
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
import { ClassifyVideoQueue } from "mq/index.ts"; import { ClassifyVideoQueue } from "mq/index.ts";
import { userExistsInBiliUsers, videoExistsInAllData } from "../../db/bilibili_metadata.ts"; import { userExistsInBiliUsers, videoExistsInAllData } from "../../db/bilibili_metadata.ts";
import { HOUR, SECOND } from "@std/datetime"; import { HOUR, SECOND } from "@core/const/time.ts";
import type { Psql } from "global.d.ts";
export async function insertVideoInfo(client: Client, aid: number) { export async function insertVideoInfo(sql: Psql, aid: number) {
const videoExists = await videoExistsInAllData(client, aid); const videoExists = await videoExistsInAllData(sql, aid);
if (videoExists) { if (videoExists) {
return; return;
} }
@ -25,34 +25,37 @@ export async function insertVideoInfo(client: Client, aid: number) {
const published_at = formatTimestampToPsql(data.View.pubdate * SECOND + 8 * HOUR); const published_at = formatTimestampToPsql(data.View.pubdate * SECOND + 8 * HOUR);
const duration = data.View.duration; const duration = data.View.duration;
const cover = data.View.pic; const cover = data.View.pic;
await client.queryObject( await sql`
`INSERT INTO bilibili_metadata (aid, bvid, description, uid, tags, title, published_at, duration, cover_url) INSERT INTO bilibili_metadata (aid, bvid, description, uid, tags, title, published_at, duration, cover_url)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, VALUES (${aid}, ${bvid}, ${desc}, ${uid}, ${tags}, ${title}, ${published_at}, ${duration}, ${cover})
[aid, bvid, desc, uid, tags, title, published_at, duration, cover], `;
); const userExists = await userExistsInBiliUsers(sql, aid);
const userExists = await userExistsInBiliUsers(client, aid);
if (!userExists) { if (!userExists) {
await client.queryObject( await sql`
`INSERT INTO bilibili_user (uid, username, "desc", fans) VALUES ($1, $2, $3, $4)`, INSERT INTO bilibili_user (uid, username, "desc", fans)
[uid, data.View.owner.name, data.Card.card.sign, data.Card.follower], VALUES (${uid}, ${data.View.owner.name}, ${data.Card.card.sign}, ${data.Card.follower})
); `;
} else { } else {
await client.queryObject( await sql`
`UPDATE bilibili_user SET fans = $1 WHERE uid = $2`, UPDATE bilibili_user SET fans = ${data.Card.follower} WHERE uid = ${uid}
[data.Card.follower, uid], `;
);
} }
const stat = data.View.stat; const stat = data.View.stat;
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},
await client.queryObject( ${stat.view},
query, ${stat.danmaku},
[aid, stat.view, stat.danmaku, stat.reply, stat.like, stat.coin, stat.share, stat.favorite], ${stat.reply},
); ${stat.like},
${stat.coin},
${stat.share},
${stat.favorite}
)
`
logger.log(`Inserted video metadata for aid: ${aid}`, "mq"); logger.log(`Inserted video metadata for aid: ${aid}`, "mq");
await ClassifyVideoQueue.add("classifyVideo", { aid }); await ClassifyVideoQueue.add("classifyVideo", { aid });

View File

@ -1,6 +1,6 @@
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts"; import { getVideoInfo } from "@core/net/getVideoInfo.ts";
import { getVideoInfo } from "net/getVideoInfo.ts"; import logger from "@core/log/logger.ts";
import logger from "log/logger.ts"; import type { Psql } from "global.d.ts";
export interface SnapshotNumber { export interface SnapshotNumber {
time: number; time: number;
@ -25,7 +25,7 @@ export interface SnapshotNumber {
* - The alicloud-fc threw an error: with error code `ALICLOUD_FC_ERROR` * - The alicloud-fc threw an error: with error code `ALICLOUD_FC_ERROR`
*/ */
export async function insertVideoSnapshot( export async function insertVideoSnapshot(
client: Client, sql: Psql,
aid: number, aid: number,
task: string, task: string,
): Promise<number | SnapshotNumber> { ): Promise<number | SnapshotNumber> {
@ -42,14 +42,10 @@ export async function insertVideoSnapshot(
const shares = data.stat.share; const shares = data.stat.share;
const favorites = data.stat.favorite; const favorites = data.stat.favorite;
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(`Taken snapshot for video ${aid}.`, "net", "fn:insertVideoSnapshot"); logger.log(`Taken snapshot for video ${aid}.`, "net", "fn:insertVideoSnapshot");

View File

@ -1,13 +1,13 @@
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
import { getLatestVideoAids } from "net/getLatestVideoAids.ts"; import { getLatestVideoAids } from "net/getLatestVideoAids.ts";
import { videoExistsInAllData } from "../../db/bilibili_metadata.ts"; import { videoExistsInAllData } from "db/bilibili_metadata.ts";
import { sleep } from "utils/sleep.ts"; import { sleep } from "utils/sleep.ts";
import { SECOND } from "@std/datetime"; import { SECOND } from "@core/const/time.ts";
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
import { LatestVideosQueue } from "mq/index.ts"; import { LatestVideosQueue } from "mq/index.ts";
import type { Psql } from "global.d.ts";
export async function queueLatestVideos( export async function queueLatestVideos(
client: Client, sql: Psql,
): Promise<number | null> { ): Promise<number | null> {
let page = 1; let page = 1;
let i = 0; let i = 0;
@ -22,7 +22,7 @@ export async function queueLatestVideos(
let allExists = true; let allExists = true;
let delay = 0; let delay = 0;
for (const aid of aids) { for (const aid of aids) {
const videoExists = await videoExistsInAllData(client, aid); const videoExists = await videoExistsInAllData(sql, aid);
if (videoExists) { if (videoExists) {
continue; continue;
} }

View File

@ -1,13 +1,13 @@
import { findClosestSnapshot, findSnapshotBefore, getLatestSnapshot } from "db/snapshotSchedule.ts"; import { findClosestSnapshot, findSnapshotBefore, getLatestSnapshot } from "db/snapshotSchedule.ts";
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts"; import { HOUR } from "@core/const/time.ts";
import { HOUR } from "@std/datetime"; import type { Psql } from "global.d.ts";
export const getRegularSnapshotInterval = async (client: Client, aid: number) => { export const getRegularSnapshotInterval = async (sql: Psql, aid: number) => {
const now = Date.now(); const now = Date.now();
const date = new Date(now - 24 * HOUR); const date = new Date(now - 24 * HOUR);
let oldSnapshot = await findSnapshotBefore(client, aid, date); let oldSnapshot = await findSnapshotBefore(sql, aid, date);
if (!oldSnapshot) oldSnapshot = await findClosestSnapshot(client, aid, date); if (!oldSnapshot) oldSnapshot = await findClosestSnapshot(sql, aid, date);
const latestSnapshot = await getLatestSnapshot(client, aid); const latestSnapshot = await getLatestSnapshot(sql, aid);
if (!oldSnapshot || !latestSnapshot) return 0; if (!oldSnapshot || !latestSnapshot) return 0;
if (oldSnapshot.created_at === latestSnapshot.created_at) return 0; if (oldSnapshot.created_at === latestSnapshot.created_at) return 0;
const hoursDiff = (latestSnapshot.created_at - oldSnapshot.created_at) / HOUR; const hoursDiff = (latestSnapshot.created_at - oldSnapshot.created_at) / HOUR;

View File

@ -1,16 +1,15 @@
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts"; import { sql } from "@core/db/dbNew";
import logger from "log/logger.ts"; import logger from "@core/log/logger.ts";
export async function removeAllTimeoutSchedules(client: Client) { export async function removeAllTimeoutSchedules() {
logger.log( logger.log(
"Too many timeout schedules, directly removing these schedules...", "Too many timeout schedules, directly removing these schedules...",
"mq", "mq",
"fn:scheduleCleanupWorker", "fn:scheduleCleanupWorker",
); );
const query: string = ` return await sql`
DELETE FROM snapshot_schedule DELETE FROM snapshot_schedule
WHERE status IN ('pending', 'processing') WHERE status IN ('pending', 'processing')
AND started_at < NOW() - INTERVAL '30 minutes' AND started_at < NOW() - INTERVAL '30 minutes'
`; `;
await client.queryObject(query);
} }

View File

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

Some files were not shown because too many files have changed in this diff Show More