1
0

merge: ref/solid into main branch

This commit is contained in:
alikia2x (寒寒) 2025-09-22 02:16:20 +08:00
commit 4e08168ef4
90 changed files with 7060 additions and 605 deletions

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

@ -30,6 +30,12 @@
<excludeFolder url="file://$MODULE_DIR$/redis" />
<excludeFolder url="file://$MODULE_DIR$/ml" />
<excludeFolder url="file://$MODULE_DIR$/src" />
<excludeFolder url="file://$MODULE_DIR$/packages/crawler/.cache" />
<excludeFolder url="file://$MODULE_DIR$/packages/solid/.vinxi" />
<excludeFolder url="file://$MODULE_DIR$/.jj" />
<excludeFolder url="file://$MODULE_DIR$/.jj/repo" />
<excludeFolder url="file://$MODULE_DIR$/.jj/working_copy" />
<excludeFolder url="file://$MODULE_DIR$/packages/solid/.output" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

View File

@ -25,6 +25,7 @@
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
<inspection_tool class="HtmlUnknownTarget" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />

6
.idea/prettier.xml Normal file
View File

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

View File

@ -9,3 +9,5 @@ MiSans.css
*.yaml
*.yml
*.mdx
packages/solid/src/drizzle/cred
packages/solid/src/drizzle/main

910
bun.lock

File diff suppressed because it is too large Load Diff

View File

@ -2,9 +2,9 @@ import networkDelegate from "@core/net/delegate.ts";
import type { VideoDetailsData, VideoDetailsResponse } from "@core/net/bilibili.d.ts";
import logger from "@core/log/logger.ts";
export async function getVideoDetails(aid: number): Promise<VideoDetailsData | null> {
export async function getVideoDetails(aid: number, archive: boolean = false): Promise<VideoDetailsData | null> {
const url = `https://api.bilibili.com/x/web-interface/view/detail?aid=${aid}`;
const data = await networkDelegate.request<VideoDetailsResponse>(url, "getVideoInfo");
const data = await networkDelegate.request<VideoDetailsResponse>(url, archive ? "" : "getVideoInfo");
const errMessage = `Error fetching metadata for ${aid}:`;
if (data.code !== 0) {
logger.error(errMessage + data.code + "-" + data.message, "net", "fn:getVideoInfo");

View File

@ -1,3 +0,0 @@
// @ts-nocheck -- skip type checking
import { _runtime } from "fumadocs-mdx"
import * as _source from "../source.config"

View File

@ -1,8 +0,0 @@
// source.config.ts
import { defineConfig } from "fumadocs-mdx/config";
var source_config_default = defineConfig({
mdxOptions: {}
});
export {
source_config_default as default
};

View File

@ -0,0 +1,11 @@
> Why do I have a folder named ".vercel" in my project?
The ".vercel" folder is created when you link a directory to a Vercel project.
> What does the "project.json" file contain?
The "project.json" file contains:
- The ID of the Vercel project that you linked ("projectId")
- The ID of the user or team your Vercel project is owned by ("orgId")
> Should I commit the ".vercel" folder?
No, you should not share the ".vercel" folder with anyone.
Upon creation, it will be automatically added to your ".gitignore" file.

View File

@ -0,0 +1 @@
{"projectId":"prj_a2fcj6ZRTyTlllCd2rFJm7kPLEOc","orgId":"team_DiIY95BaFppaGJqqgrXYNt5O","projectName":"cvsa-theme"}

28
packages/solid/.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
dist
.wrangler
.output
.vercel
.netlify
.vinxi
app.config.timestamp_*.js
# Environment
.env
.env*.local
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
*.launch
.settings/
# Temp
gitignore
# System Files
.DS_Store
Thumbs.db

View File

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

View File

@ -0,0 +1,18 @@
"use server";
import { defineConfig } from "@solidjs/start/config";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
vite: {
plugins: [tsconfigPaths()],
optimizeDeps: {
include: ["@m3-components/solid"],
esbuildOptions: {
jsx: "automatic",
jsxDev: true,
jsxImportSource: "solid-js/h"
}
}
}
});

1785
packages/solid/bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,10 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
out: "./src/drizzle/cred",
schema: "./src/db/schema.ts",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL_CRED!
}
});

View File

@ -0,0 +1,11 @@
import "dotenv/config";
import { defineConfig } from "drizzle-kit";
export default defineConfig({
out: "./src/drizzle/main",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL_MAIN!
},
tablesFilter: ["*"],
});

View File

@ -0,0 +1,45 @@
{
"name": "solid",
"scripts": {
"dev": "vinxi dev --port 7400 --host",
"build": "vinxi build",
"start": "bunx node-env-run --exec bun -- run vinxi start --port 7400",
"version": "vinxi version",
"format": "prettier . --write"
},
"dependencies": {
"@m3-components/solid": "0.2.6",
"@solid-primitives/media": "^2.3.3",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.3",
"@solidjs/start": "^1.1.7",
"@types/luxon": "^3.7.1",
"@ungap/has-own": "^0.1.1",
"animejs": "^4.1.2",
"axios": "^1.11.0",
"dotenv": "^16.6.1",
"drizzle-orm": "^0.44.4",
"luxon": "^3.7.1",
"minimatch": "^10.0.3",
"postgres": "^3.4.7",
"solid-js": "^1.9.8",
"tailwind-variants": "^1.0.0",
"tailwindcss": "3",
"vinxi": "^0.5.8"
},
"engines": {
"node": ">=22"
},
"devDependencies": {
"@csstools/postcss-bundler": "^2.0.8",
"@csstools/postcss-cascade-layers": "^5.0.2",
"@csstools/postcss-oklab-function": "^4.0.10",
"@tailwindcss/postcss": "^4.1.11",
"autoprefixer": "^10.4.21",
"drizzle-kit": "^0.31.4",
"postcss": "^8.5.6",
"tsx": "^4.20.3",
"vite-plugin-solid": "^2.11.8",
"vite-tsconfig-paths": "^5.1.4"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

241
packages/solid/src/app.css Normal file
View File

@ -0,0 +1,241 @@
@import url("https://assets.projectcvsa.com/hm-sans/index.css");
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--md-sys-color-background: #fff8f6;
--md-sys-color-on-background: #2a1613;
--md-sys-color-surface: #fff8f6;
--md-sys-color-surface-dim: #f7d2cc;
--md-sys-color-surface-bright: #fff8f6;
--md-sys-color-surface-container-lowest: #ffffff;
--md-sys-color-surface-container-low: #fff0ee;
--md-sys-color-surface-container: #ffe9e6;
--md-sys-color-surface-container-high: #ffe2dd;
--md-sys-color-surface-container-highest: #ffdad4;
--md-sys-color-on-surface: #2a1613;
--md-sys-color-surface-variant: #ffdad4;
--md-sys-color-on-surface-variant: #5f3e39;
--md-sys-color-inverse-surface: #422b27;
--md-sys-color-inverse-on-surface: #ffedea;
--md-sys-color-outline: #946e68;
--md-sys-color-outline-variant: #eabcb4;
--md-sys-color-shadow: #000000;
--md-sys-color-scrim: #000000;
--md-sys-color-surface-tint: #c00100;
--md-sys-color-primary: #ee0000;
--md-sys-color-on-primary: #ffffff;
--md-sys-color-primary-container: #ee0000;
--md-sys-color-on-primary-container: #ffffff;
--md-sys-color-inverse-primary: #ffb4a8;
--md-sys-color-secondary: #b4271a;
--md-sys-color-on-secondary: #ffffff;
--md-sys-color-secondary-container: #ff7460;
--md-sys-color-on-secondary-container: #2f0000;
--md-sys-color-tertiary: #6f4800;
--md-sys-color-on-tertiary: #ffffff;
--md-sys-color-tertiary-container: #9f6900;
--md-sys-color-on-tertiary-container: #ffffff;
--md-sys-color-error: #ba1a1a;
--md-sys-color-on-error: #ffffff;
--md-sys-color-error-container: #ffdad6;
--md-sys-color-on-error-container: #410002;
}
@media (prefers-color-scheme: light) {
:root {
--md-sys-color-background: #fff8f6;
--md-sys-color-on-background: #2a1613;
--md-sys-color-surface: #fff8f6;
--md-sys-color-surface-dim: #f7d2cc;
--md-sys-color-surface-bright: #fff8f6;
--md-sys-color-surface-container-lowest: #ffffff;
--md-sys-color-surface-container-low: #fff0ee;
--md-sys-color-surface-container: #ffe9e6;
--md-sys-color-surface-container-high: #ffe2dd;
--md-sys-color-surface-container-highest: #ffdad4;
--md-sys-color-on-surface: #2a1613;
--md-sys-color-surface-variant: #ffdad4;
--md-sys-color-on-surface-variant: #5f3e39;
--md-sys-color-inverse-surface: #422b27;
--md-sys-color-inverse-on-surface: #ffedea;
--md-sys-color-outline: #946e68;
--md-sys-color-outline-variant: #eabcb4;
--md-sys-color-shadow: #000000;
--md-sys-color-scrim: #000000;
--md-sys-color-surface-tint: #c00100;
--md-sys-color-primary: #ee0000;
--md-sys-color-on-primary: #ffffff;
--md-sys-color-primary-container: #ee0000;
--md-sys-color-on-primary-container: #ffffff;
--md-sys-color-inverse-primary: #ffb4a8;
--md-sys-color-secondary: #b4271a;
--md-sys-color-on-secondary: #ffffff;
--md-sys-color-secondary-container: #ff7460;
--md-sys-color-on-secondary-container: #2f0000;
--md-sys-color-tertiary: #6f4800;
--md-sys-color-on-tertiary: #ffffff;
--md-sys-color-tertiary-container: #9f6900;
--md-sys-color-on-tertiary-container: #ffffff;
--md-sys-color-error: #ba1a1a;
--md-sys-color-on-error: #ffffff;
--md-sys-color-error-container: #ffdad6;
--md-sys-color-on-error-container: #410002;
}
}
@media (prefers-color-scheme: dark) {
:root {
--md-sys-color-background: #210e0b;
--md-sys-color-on-background: #ffdad4;
--md-sys-color-surface: #210e0b;
--md-sys-color-surface-dim: #210e0b;
--md-sys-color-surface-bright: #4b332f;
--md-sys-color-surface-container-lowest: #1b0907;
--md-sys-color-surface-container-low: #2a1613;
--md-sys-color-surface-container: #2f1a17;
--md-sys-color-surface-container-high: #3a2421;
--md-sys-color-surface-container-highest: #462f2b;
--md-sys-color-on-surface: #ffdad4;
--md-sys-color-surface-variant: #5f3e39;
--md-sys-color-on-surface-variant: #eabcb4;
--md-sys-color-inverse-surface: #ffdad4;
--md-sys-color-inverse-on-surface: #422b27;
--md-sys-color-outline: #b08780;
--md-sys-color-outline-variant: #5f3e39;
--md-sys-color-shadow: #000000;
--md-sys-color-scrim: #000000;
--md-sys-color-surface-tint: #ffb4a8;
--md-sys-color-primary: #ffb4a8;
--md-sys-color-on-primary: #690000;
--md-sys-color-primary-container: #de0000;
--md-sys-color-on-primary-container: #ffffff;
--md-sys-color-inverse-primary: #c00100;
--md-sys-color-secondary: #ffb4a8;
--md-sys-color-on-secondary: #690000;
--md-sys-color-secondary-container: #870100;
--md-sys-color-on-secondary-container: #ffc9c0;
--md-sys-color-tertiary: #feba54;
--md-sys-color-on-tertiary: #452b00;
--md-sys-color-tertiary-container: #966300;
--md-sys-color-on-tertiary-container: #ffffff;
--md-sys-color-error: #ffb4ab;
--md-sys-color-on-error: #690005;
--md-sys-color-error-container: #93000a;
--md-sys-color-on-error-container: #ffdad6;
}
}
.gradient-blur {
inset: auto 0 0 0;
pointer-events: none;
}
.gradient-blur > div,
.gradient-blur::before,
.gradient-blur::after {
position: absolute;
inset: 0;
}
.gradient-blur::before {
content: "";
z-index: 1;
backdrop-filter: blur(0.5px);
mask: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 1) 12.5%,
rgba(0, 0, 0, 1) 25%,
rgba(0, 0, 0, 0) 37.5%
);
}
.gradient-blur > div:nth-of-type(1) {
z-index: 2;
backdrop-filter: blur(1px);
mask: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 12.5%,
rgba(0, 0, 0, 1) 25%,
rgba(0, 0, 0, 1) 37.5%,
rgba(0, 0, 0, 0) 50%
);
}
.gradient-blur > div:nth-of-type(2) {
z-index: 3;
backdrop-filter: blur(2px);
mask: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 25%,
rgba(0, 0, 0, 1) 37.5%,
rgba(0, 0, 0, 1) 50%,
rgba(0, 0, 0, 0) 62.5%
);
}
.gradient-blur > div:nth-of-type(3) {
z-index: 4;
backdrop-filter: blur(4px);
mask: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 37.5%,
rgba(0, 0, 0, 1) 50%,
rgba(0, 0, 0, 1) 62.5%,
rgba(0, 0, 0, 0) 75%
);
}
.gradient-blur > div:nth-of-type(4) {
z-index: 5;
backdrop-filter: blur(8px);
mask: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 50%,
rgba(0, 0, 0, 1) 62.5%,
rgba(0, 0, 0, 1) 75%,
rgba(0, 0, 0, 0) 87.5%
);
}
.gradient-blur > div:nth-of-type(5) {
z-index: 6;
backdrop-filter: blur(13px);
mask: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 62.5%,
rgba(0, 0, 0, 1) 75%,
rgba(0, 0, 0, 1) 87.5%,
rgba(0, 0, 0, 0) 100%
);
}
.gradient-blur > div:nth-of-type(6) {
z-index: 7;
backdrop-filter: blur(24px);
mask: linear-gradient(to bottom, rgba(0, 0, 0, 0) 75%, rgba(0, 0, 0, 1) 87.5%, rgba(0, 0, 0, 1) 100%);
}
.gradient-blur::after {
content: "";
z-index: 8;
backdrop-filter: blur(48px);
mask: linear-gradient(to bottom, rgba(0, 0, 0, 0) 87.5%, rgba(0, 0, 0, 1) 100%);
}
a:not(.clear) {
@apply text-primary;
}
:root {
font-family: "HarmonyOS Sans SC", sans-serif;
@apply bg-surface text-on-surface;
}
@font-face {
font-family: "IPSD";
src: url("/fonts/IBMPlexSansCondDigits-Medium.woff2") format("woff2");
font-weight: 500;
font-style: normal;
}
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
appearance: none;
}

View File

@ -0,0 +1,40 @@
// polyfill
import "@ungap/has-own";
import { MetaProvider } from "@solidjs/meta";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { onMount, Suspense } from "solid-js";
import "@m3-components/solid/index.css";
import "./app.css";
import { setActiveTab, tabMap } from "./components/layout/Navigation";
import { minimatch } from "minimatch";
export const refreshTab = (path: string) => {
for (const [key, value] of Object.entries(tabMap)) {
if (!minimatch(path, key)) continue;
setActiveTab(value);
break;
}
};
export default function App() {
onMount(() => {
refreshTab(location.pathname);
window.addEventListener("popstate", (event) => {
refreshTab(location.pathname);
});
});
return (
<Router
root={(props) => (
<MetaProvider>
<Suspense>{props.children}</Suspense>
</MetaProvider>
)}
>
<FileRoutes />
</Router>
);
}

View File

@ -0,0 +1,4 @@
import { JSX } from "solid-js";
export type DivProps = JSX.HTMLAttributes<HTMLDivElement>;
export type ElementProps = JSX.HTMLAttributes<HTMLElement>;

View File

@ -0,0 +1,23 @@
import { SVGIconComponent } from "./types";
export const AlbumIcon: SVGIconComponent = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M12 16.5q1.875 0 3.188-1.312T16.5 12t-1.312-3.187T12 7.5T8.813 8.813T7.5 12t1.313 3.188T12 16.5m0-3.5q-.425 0-.712-.288T11 12t.288-.712T12 11t.713.288T13 12t-.288.713T12 13m0 9q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8"
/>
</svg>
);
};
export const AlbumFilledIcon: SVGIconComponent = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M12 16.5q1.875 0 3.188-1.312T16.5 12t-1.312-3.187T12 7.5T8.813 8.813T7.5 12t1.313 3.188T12 16.5m0-3.5q-.425 0-.712-.288T11 12t.288-.712T12 11t.713.288T13 12t-.288.713T12 13m0 9q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22"
/>
</svg>
);
};

View File

@ -0,0 +1,12 @@
import { SVGIconComponent } from "~/components/icons/types";
export const RightArrow: SVGIconComponent = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 28 28" {...props}>
<path
fill="currentColor"
d="M18.84 15.17h-13c-.34 0-.61-.12-.84-.34-.22-.22-.33-.5-.33-.83a1.13 1.13 0 0 1 1.17-1.17h13l-3.32-3.32c-.24-.23-.35-.5-.34-.82.01-.3.12-.58.34-.81.23-.24.5-.36.83-.37.32 0 .6.1.83.34l5.34 5.33c.11.12.2.25.25.38a1.34 1.34 0 0 1 0 .88 1 1 0 0 1-.25.38l-5.34 5.33c-.23.24-.51.35-.83.34a1.19 1.19 0 0 1-1.17-1.18c0-.31.1-.58.34-.82l3.32-3.32Z"
/>
</svg>
);
};

View File

@ -0,0 +1,12 @@
import { SVGIconComponent } from "./types";
export const EditIcon: SVGIconComponent = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M5 19h1.425L16.2 9.225L14.775 7.8L5 17.575zm-2 2v-4.25L16.2 3.575q.3-.275.663-.425t.762-.15t.775.15t.65.45L20.425 5q.3.275.438.65T21 6.4q0 .4-.137.763t-.438.662L7.25 21zM19 6.4L17.6 5zm-3.525 2.125l-.7-.725L16.2 9.225z"
/>
</svg>
);
};

View File

@ -0,0 +1,12 @@
import { SVGIconComponent } from "./types";
export const HistoryIcon: SVGIconComponent = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M12 21q-3.15 0-5.575-1.912T3.275 14.2q-.1-.375.15-.687t.675-.363q.4-.05.725.15t.45.6q.6 2.25 2.475 3.675T12 19q2.925 0 4.963-2.037T19 12t-2.037-4.962T12 5q-1.725 0-3.225.8T6.25 8H8q.425 0 .713.288T9 9t-.288.713T8 10H4q-.425 0-.712-.288T3 9V5q0-.425.288-.712T4 4t.713.288T5 5v1.35q1.275-1.6 3.113-2.475T12 3q1.875 0 3.513.713t2.85 1.924t1.925 2.85T21 12t-.712 3.513t-1.925 2.85t-2.85 1.925T12 21m1-9.4l2.5 2.5q.275.275.275.7t-.275.7t-.7.275t-.7-.275l-2.8-2.8q-.15-.15-.225-.337T11 11.975V8q0-.425.288-.712T12 7t.713.288T13 8z"
/>
</svg>
);
};

View File

@ -0,0 +1,23 @@
import { SVGIconComponent } from "./types";
export const HomeFilledIcon: SVGIconComponent = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M4 19v-9q0-.475.213-.9t.587-.7l6-4.5q.525-.4 1.2-.4t1.2.4l6 4.5q.375.275.588.7T20 10v9q0 .825-.588 1.413T18 21h-3q-.425 0-.712-.288T14 20v-5q0-.425-.288-.712T13 14h-2q-.425 0-.712.288T10 15v5q0 .425-.288.713T9 21H6q-.825 0-1.412-.587T4 19"
/>
</svg>
);
};
export const HomeIcon: SVGIconComponent = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M6 19h3v-5q0-.425.288-.712T10 13h4q.425 0 .713.288T15 14v5h3v-9l-6-4.5L6 10zm-2 0v-9q0-.475.213-.9t.587-.7l6-4.5q.525-.4 1.2-.4t1.2.4l6 4.5q.375.275.588.7T20 10v9q0 .825-.588 1.413T18 21h-4q-.425 0-.712-.288T13 20v-5h-2v5q0 .425-.288.713T10 21H6q-.825 0-1.412-.587T4 19m8-6.75"
/>
</svg>
);
};

View File

@ -0,0 +1,12 @@
import { SVGIconComponent } from "./types";
export const LinkIcon: SVGIconComponent = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M11 17H7a4.82 4.82 0 0 1-3.54-1.46A4.82 4.82 0 0 1 2 12c0-1.38.49-2.56 1.46-3.54A4.82 4.82 0 0 1 7 7h4v2H7c-.83 0-1.54.3-2.13.88A2.9 2.9 0 0 0 4 12c0 .83.3 1.54.88 2.13.58.58 1.29.87 2.12.87h4v2Zm-3-4v-2h8v2H8Zm5 4v-2h4c.83 0 1.54-.3 2.13-.88.58-.58.87-1.29.87-2.12 0-.83-.3-1.54-.88-2.13A2.9 2.9 0 0 0 17 9h-4V7h4c1.38 0 2.56.49 3.54 1.46A4.82 4.82 0 0 1 22 12c0 1.38-.49 2.56-1.46 3.54A4.8 4.8 0 0 1 17 17h-4Z"
/>
</svg>
);
};

View File

@ -0,0 +1,12 @@
import { SVGIconComponent } from "./types";
export const MusicIcon: SVGIconComponent = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<path
d="M13.82 13.82q.58-.58.58-1.42V8H16q.35 0 .57-.23a.8.8 0 0 0 .23-.57.8.8 0 0 0-.23-.57.8.8 0 0 0-.57-.23h-1.6a.8.8 0 0 0-.57.23.8.8 0 0 0-.23.57v3.6a2 2 0 0 0-1.2-.4q-.84 0-1.42.58t-.58 1.42.58 1.42 1.42.58 1.42-.58M8.8 16.8q-.66 0-1.13-.47a1.5 1.5 0 0 1-.47-1.13V5.6q0-.66.47-1.13T8.8 4h9.6q.66 0 1.13.47T20 5.6v9.6q0 .66-.47 1.13t-1.13.47zm0-1.6h9.6V5.6H8.8zM5.6 20q-.66 0-1.13-.47A1.5 1.5 0 0 1 4 18.4V8q0-.34.23-.57a.8.8 0 0 1 .57-.23q.35 0 .57.23.23.22.23.57v10.4H16q.34 0 .57.23.23.22.23.57a.8.8 0 0 1-.8.8zM8.8 5.6v9.6z"
fill="currentColor"
/>
</svg>
);
};

View File

@ -0,0 +1,12 @@
import { SVGIconComponent } from "~/components/icons/types";
export const SearchIcon: SVGIconComponent = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14"
/>
</svg>
);
};

View File

@ -0,0 +1,34 @@
import { SVGIconComponent } from "./types";
export const StarBadge4: SVGIconComponent = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M17.78 4c.78 0 1.05.08 1.34.23.28.15.5.37.65.65.15.29.23.56.23 1.34v11.56c0 .78-.08 1.05-.23 1.34-.15.28-.37.5-.65.65-.29.15-.56.23-1.34.23H6.22c-.78 0-1.05-.08-1.34-.23-.28-.15-.5-.37-.65-.65-.15-.29-.23-.56-.23-1.34V6.22c0-.78.08-1.05.23-1.34.15-.28.37-.5.65-.65.29-.15.56-.23 1.34-.23h11.56ZM12 8a.45.45 0 0 0-.45.31l-.78 1.84a.43.43 0 0 1-.36.26l-1.97.16c-.24.02-.39.13-.46.34a.5.5 0 0 0 .15.54l1.52 1.31c.12.1.17.26.13.42l-.44 1.93c-.05.22.01.4.19.52.17.12.35.13.54 0l1.71-1.01a.4.4 0 0 1 .44 0l1.71 1.02c.19.12.37.11.54-.01.18-.13.24-.3.19-.52l-.44-1.93a.43.43 0 0 1 .13-.42l1.52-1.31a.48.48 0 0 0 .15-.54c-.07-.21-.22-.32-.46-.34l-1.97-.16a.43.43 0 0 1-.36-.26l-.78-1.84a.45.45 0 0 0-.45-.3Z"
/>
</svg>
);
};
export const StarBadge6: SVGIconComponent = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M12.52 2.64c.17.1.3.23.4.4l1.97 3.42h4.54c.18 0 .36.05.52.14h.01c.52.3.7.96.4 1.47L18.09 12l2.25 3.9a1.07 1.07 0 0 1-.93 1.6h-4.5l-1.97 3.43c-.1.16-.22.3-.38.39h-.01c-.52.3-1.17.13-1.47-.39l-1.97-3.42H4.57a1.07 1.07 0 0 1-.93-1.61l2.27-3.93-2.25-3.9a1.07 1.07 0 0 1-.14-.52v-.01c0-.6.48-1.08 1.07-1.08h4.5l1.97-3.42c.3-.52.95-.7 1.46-.4ZM12 8.61a.4.4 0 0 0-.39.25l-.67 1.6a.43.43 0 0 1-.36.26l-1.7.13c-.2.01-.34.11-.4.3a.42.42 0 0 0 .14.47l1.3 1.13c.12.1.17.26.13.42l-.38 1.66c-.04.19.01.34.17.45.15.11.3.11.47.01l1.47-.88c.14-.08.3-.08.44 0l1.47.88c.17.1.32.1.48 0a.4.4 0 0 0 .16-.45l-.38-1.67a.43.43 0 0 1 .14-.42l1.3-1.13c.14-.13.19-.29.13-.47s-.19-.28-.39-.3l-1.7-.13a.43.43 0 0 1-.36-.26l-.67-1.59a.4.4 0 0 0-.4-.26Z"
/>
</svg>
);
};
export const StarBadge8: SVGIconComponent = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="m12.22 14.25 1.43.86c.16.1.31.1.46-.01a.4.4 0 0 0 .16-.44l-.37-1.62a.42.42 0 0 1 .14-.41l1.26-1.1a.4.4 0 0 0 .13-.46c-.06-.18-.19-.28-.39-.3l-1.65-.12a.42.42 0 0 1-.35-.26l-.65-1.54a.39.39 0 0 0-.39-.26.39.39 0 0 0-.39.26l-.65 1.54a.42.42 0 0 1-.35.26l-1.65.13c-.2.01-.33.1-.39.29a.4.4 0 0 0 .13.46l1.26 1.1c.12.1.18.26.14.41l-.37 1.62c-.04.18 0 .33.16.44.15.1.3.1.46 0l1.43-.85c.14-.08.3-.08.44 0Zm-3.27 4.6h-2.1c-.46 0-.87-.16-1.2-.5-.34-.33-.5-.74-.5-1.2v-2.1a.42.42 0 0 0-.13-.3L3.5 13.2a1.93 1.93 0 0 1-.36-.57 1.64 1.64 0 0 1 0-1.26c.08-.2.2-.4.36-.57l1.53-1.55a.42.42 0 0 0 .12-.3v-2.1c0-.46.17-.87.5-1.2.34-.34.75-.5 1.22-.5h2.1c.1 0 .21-.05.3-.13L10.8 3.5c.17-.15.36-.28.57-.36a1.64 1.64 0 0 1 1.26 0c.21.08.4.2.57.36l1.55 1.53a.4.4 0 0 0 .3.12h2.1c.46 0 .87.17 1.2.5.34.34.5.75.5 1.22v2.1c0 .1.05.21.13.3l1.53 1.54c.15.17.28.36.36.57a1.64 1.64 0 0 1 0 1.26c-.08.21-.2.4-.36.57l-1.53 1.55a.42.42 0 0 0-.12.3v2.1c0 .46-.17.87-.5 1.2-.34.34-.75.5-1.22.5h-2.1a.42.42 0 0 0-.3.13L13.2 20.5a1.9 1.9 0 0 1-.57.36 1.64 1.64 0 0 1-1.26 0c-.2-.08-.4-.2-.57-.36l-1.55-1.53a.42.42 0 0 0-.3-.12Z"
/>
</svg>
);
};

View File

@ -0,0 +1,5 @@
export * from "./Home";
export * from "./Music";
export * from "./Album";
export * from "./Search";
export * from "./Edit";

View File

@ -0,0 +1,3 @@
import { Component, JSX } from "solid-js";
type SVGIconComponent = Component<JSX.SvgSVGAttributes<SVGSVGElement>>;

View File

@ -0,0 +1,10 @@
import { Component } from "solid-js";
import { DivProps } from "~/components/common";
export const BodyRegion: Component<DivProps> = (props) => {
return (
<div class="w-full min-h-full" {...props}>
{props.children}
</div>
);
};

View File

@ -0,0 +1,37 @@
import { Component } from "solid-js";
import { DynamicImage } from "~/components/utils/DynamicImage";
import {
AppBar,
AppBarLeadingElement,
AppBarSearchBox,
AppBarSearchContainer,
AppBarTrailingElement,
AppBarTrailingElementGroup,
IconButton
} from "@m3-components/solid";
export const NavigationDesktop: Component = () => {
return (
<AppBar class="hidden lg:flex h-20 xl:h-22 2xl:h-24 z-20" variant="search">
<AppBarLeadingElement class="ml-4 h-full grow shrink basis-0">
<DynamicImage
class="lg:block h-full"
darkSrc="/icons/zh/appbar_desktop_dark.svg"
lightSrc="/icons/zh/appbar_desktop_light.svg"
/>
</AppBarLeadingElement>
<AppBarSearchContainer>
<AppBarSearchBox
class="mx-auto text-center placeholder-on-surface-variant text-on-surface
placeholder:font-light"
placeholder="搜索"
/>
</AppBarSearchContainer>
<AppBarTrailingElementGroup class="h-full grow shrink basis-0">
<AppBarTrailingElement>
<IconButton></IconButton>
</AppBarTrailingElement>
</AppBarTrailingElementGroup>
</AppBar>
);
};

View File

@ -0,0 +1,114 @@
import { Component, createEffect, createSignal, For } from "solid-js";
import {
NavigationRailFAB,
NavigationRail,
NavigationRailAction,
NavigationRailActions,
NavigationRailMenu,
AppBar,
AppBarLeadingElement,
AppBarSearchBox,
AppBarTrailingElementGroup,
AppBarTrailingElement,
IconButton,
AppBarSearchContainer,
ExtendedFAB
} from "@m3-components/solid";
import { A } from "@solidjs/router";
import { SearchIcon } from "~/components/icons/Search";
import { Portal } from "solid-js/web";
import { animate } from "animejs";
import { actions, actionsEn, activeTab, navigationExpanded, searchT, setActiveTab, setNavigationExpanded } from ".";
export const NavigationMobile: Component<{ lang?: "zh" | "en" }> = (props) => {
const [el, setEl] = createSignal<HTMLElement | null>(null);
createEffect(() => {
if (!el) return;
if (navigationExpanded()) {
animate(el()!, {
x: 0,
duration: 500,
z: 100,
ease: "cubicBezier(0.27, 1.06, 0.18, 1.00)"
});
} else {
animate(el()!, {
x: -380,
duration: 500,
z: 0,
ease: "cubicBezier(0.27, 1.06, 0.18, 1.00)"
});
}
});
return (
<>
<NavigationRailMenu
class="top-3 left-4 fixed z-[100] backdrop-blur-md shadow-xl lg:hidden"
onClick={() => {
setNavigationExpanded(!navigationExpanded());
}}
/>
<AppBar class="z-20 lg:hidden" variant="search">
<AppBarLeadingElement>
<NavigationRailMenu class="invisible" />
</AppBarLeadingElement>
<AppBarSearchContainer class="max-sm:w-[calc(100%-7.9rem)]">
<AppBarSearchBox placeholder="搜索" class="placeholder-on-surface-variant text-on-surface" />
</AppBarSearchContainer>
<AppBarTrailingElementGroup>
<AppBarTrailingElement>
<IconButton></IconButton>
</AppBarTrailingElement>
</AppBarTrailingElementGroup>
</AppBar>
<Portal mount={document.getElementById("modal") || undefined}>
<div
class="fixed lg:hidden top-0 left-0 h-full z-50"
style="transform: translateX(-300px);"
ref={(el) => {
setEl(el);
}}
>
<NavigationRail
class="z-20 top-0 bg-background overflow-auto rounded-r-2xl shadow-shadow shadow-2xl"
width={256}
expanded={true}
>
<ExtendedFAB
text={searchT[props.lang || "zh"]}
class="left-5 top-5 font-medium leading-6 duration-100 whitespace-nowrap
transition-none w-24 h-14 text-base mt-6"
color="primary"
position="unset"
>
<SearchIcon />
</ExtendedFAB>
<NavigationRailActions>
<For each={props.lang == "en" ? actionsEn : actions}>
{(action, index) => (
<A href={action.href} class="clear">
<NavigationRailAction
activated={activeTab() == index()}
label={action.label}
icon={action.icon}
onClick={() => {
setNavigationExpanded(false);
setActiveTab(index);
}}
/>
</A>
)}
</For>
</NavigationRailActions>
</NavigationRail>
<div
onclick={() => setNavigationExpanded(false)}
class="w-screen h-screen z-10 absoluter bg-transparent"
></div>
</div>
</Portal>
</>
);
};

View File

@ -0,0 +1,66 @@
import { Component, createSignal } from "solid-js";
import { AlbumIcon, HomeIcon, MusicIcon } from "~/components/icons";
export const [activeTab, setActiveTab] = createSignal(-1);
export const [navigationExpanded, setNavigationExpanded] = createSignal(false);
interface Action {
icon: Component;
label: string;
href: string;
}
export const actions: Action[] = [
{
icon: HomeIcon,
label: "主页",
href: "/"
},
{
icon: MusicIcon,
label: "歌曲",
href: "/songs"
},
{
icon: AlbumIcon,
label: "专辑",
href: "/albums"
}
];
export const actionsEn: Action[] = [
{
icon: HomeIcon,
label: "Home",
href: "/en/"
},
{
icon: MusicIcon,
label: "Songs",
href: "/en/songs"
},
{
icon: AlbumIcon,
label: "Albums",
href: "/en/albums"
}
];
export const tabMap = {
"/": 0,
"/song*": 1,
"/song/**/*": 1,
"/albums": 2,
"/album/**/*": 2,
"/en/": 0,
"/en/songs": 1,
"/en/song*": 1,
"/en/song/**/*": 1,
"/en/albums": 2,
"/en/album/**/*": 2
};
export const searchT = {
zh: "搜索",
en: "Search"
};

View File

@ -0,0 +1,28 @@
import { NavigationMobile } from "./Navigation/Mobile";
import { DivProps } from "../common";
import { Component } from "solid-js";
import { BeforeLeaveEventArgs, useBeforeLeave } from "@solidjs/router";
import { refreshTab } from "~/app";
import { NavigationDesktop } from "./Navigation/Desktop";
import { BodyRegion } from "./Body";
interface LayoutProps extends DivProps {
lang?: "zh" | "en";
}
export const Layout: Component<LayoutProps> = (props) => {
useBeforeLeave((e: BeforeLeaveEventArgs) => {
if (typeof e.to === "number") {
refreshTab(e.to.toString());
return;
}
refreshTab(e.to);
});
return (
<div class="relatve w-screen max-w-full min-h-screen">
<NavigationMobile lang={props.lang} />
<NavigationDesktop />
<BodyRegion>{props.children}</BodyRegion>
</div>
);
};

View File

@ -0,0 +1,48 @@
import { dbCred } from "~db/index";
import { loginSessions, users } from "~db/cred/schema";
import { and, eq, gt, isNull, sql } from "drizzle-orm";
import { SensitiveUserFields, UserType } from "~db/outerSchema";
type ReturnedUser = Omit<UserType, SensitiveUserFields>;
export const getUserLoggedin = async (sessionID?: string): Promise<ReturnedUser | null> => {
if (!sessionID) {
return null;
}
const session = await dbCred
.select({
uid: loginSessions.uid
})
.from(loginSessions)
.where(
and(
eq(loginSessions.id, sessionID),
gt(loginSessions.expireAt, sql`now()`),
isNull(loginSessions.deactivatedAt)
)
)
.limit(1);
if (session.length === 0) {
return null;
}
const uid = session[0].uid;
const user: ReturnedUser[] = await dbCred
.select({
id: users.id,
username: users.username,
nickname: users.nickname,
role: users.role,
createdAt: users.createdAt
})
.from(users)
.where(eq(users.id, uid))
.limit(1);
if (user.length === 0) {
return null;
}
console.log("Query for sessionID:", sessionID);
return user[0];
};

View File

@ -0,0 +1,34 @@
import { Accessor, Component, createSignal } from "solid-js";
import { createContext, useContext } from "solid-js";
type Hook = {
memoizedValue: any | null;
deps: any[] | null;
promise: Promise<any> | null;
};
export type RequestContextValue = Map<string, Hook>;
export type Context = [Accessor<RequestContextValue>, (v: RequestContextValue) => void];
export const RequestContext = createContext<Context | null>(null);
export const RequestContextProvider: Component<{ children: any }> = (props) => {
const initValue: RequestContextValue = new Map();
const [value, setValue] = createSignal(initValue);
const updateValue = (v: RequestContextValue) => {
setValue(v);
};
const context: Context = [value, updateValue];
return <RequestContext.Provider value={context}>{props.children}</RequestContext.Provider>;
};
export function useRequestContext(): Context {
const ctx = useContext(RequestContext);
if (!ctx) {
throw new Error("useRequestContext must be used within a RequestContextProvider");
}
return ctx;
}

View File

@ -0,0 +1,148 @@
import { Component } from "solid-js";
import { Card, CardContent, CardMedia, Typography } from "@m3-components/solid";
import { TabSwitcher } from "~/components/song/TabSwitcher";
import { Staff } from "~/components/song/Staff";
import { SongType } from "~db/outerSchema";
export const Content: Component<{data: SongType | null}> = (props) => {
return (
<>
<Card variant="outlined" class="w-full max-lg:rounded-none max-lg:border-none">
<CardMedia
round={false}
src={props.data?.image || ""}
referrerpolicy="no-referrer"
class="relative w-full z-[2] max-lg:hidden"
/>
<div class="relative w-full overflow-hidden lg:hidden">
<CardMedia
round={false}
src={props.data?.image || ""}
referrerpolicy="no-referrer"
class="relative w-full z-[2]"
/>
<div class="h-10 lg:h-0" />
<CardMedia
round={false}
src={props.data?.image || ""}
referrerpolicy="no-referrer"
class="w-full absolute lg:hidden top-10 z-[1]"
/>
<span
class="left-3 absolute bottom-14 z-10 text-sm text-white/95"
style="text-shadow:0px 1px 1px rgb(0 0 0 / 0.2) "
>
&
</span>
<span
class="left-3 absolute bottom-3 z-10 font-medium text-4xl text-white/90 "
style="text-shadow: 0px 1px 0px rgb(0 0 0 / 0.075), 0px 1px 1px rgb(0 0 0 / 0.075), 0px 2px 2px rgb(0 0 0 / 0.075)"
>
</span>
<span
class="font-[Inter] right-3 absolute bottom-10 z-10 text-xl text-white/95"
style="text-shadow: 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1)"
>
4:54
</span>
<span
class="font-[Inter] right-3 absolute bottom-3 z-10 text-xl text-white/95"
style="text-shadow: 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1)"
>
12,422
<span class="ml-1 text-sm"></span>
</span>
<div class="lg:hidden w-full gradient-blur !absolute !h-32">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<CardContent class="max-lg:hidden px-7 py-6 flex flex-col gap-4">
<Typography.Display class="leading-[2.75rem]" variant="small">
{props.data?.name}
</Typography.Display>
<div class="grid grid-cols-2 grid-rows-3 gap-2">
<div class="flex flex-col">
<Typography.Label class="text-on-surface-variant" variant="large">
</Typography.Label>
<Typography.Body variant="large">
<a href="#"></a>
</Typography.Body>
</div>
<div class="flex flex-col">
<Typography.Label class="text-on-surface-variant" variant="large">
</Typography.Label>
<Typography.Body variant="large">4:28</Typography.Body>
</div>
<div class="flex flex-col">
<Typography.Label class="text-on-surface-variant" variant="large">
稿
</Typography.Label>
<Typography.Body variant="large">
<a href="#"></a>
</Typography.Body>
</div>
<div class="flex flex-col">
<Typography.Label class="text-on-surface-variant" variant="large">
</Typography.Label>
<Typography.Body class="flex gap-2" variant="large">
<a href="https://www.bilibili.com/video/BV1eaq9Y3EVV/"></a>
<a href="https://vocadb.net/S/742394">VocaDB</a>
</Typography.Body>
</div>
<div class="flex flex-col">
<Typography.Label class="text-on-surface-variant" variant="large">
</Typography.Label>
<Typography.Body variant="large">2024-12-15 12:15:00</Typography.Body>
</div>
<div class="flex flex-col">
<Typography.Label class="text-on-surface-variant" variant="large">
</Typography.Label>
<Typography.Body variant="large">1.24 (12,422)</Typography.Body>
</div>
</div>
</CardContent>
</Card>
<div class="mx-1 my-6 lg:hidden">
<TabSwitcher />
</div>
<article class="mt-6">
<Typography.Headline class="mx-4" variant="medium">
</Typography.Headline>
<Typography.Body class="mx-4 mt-2" variant="large">
<span class="font-medium"></span><a href="#"></a>
<span>
&VeryThinSpace;2024&VeryThinSpace;&VeryThinSpace;12&VeryThinSpace;&VeryThinSpace;15&VeryThinSpace;
</span>
稿
<a href="#"></a>&ThinSpace;<a href="#">Synthesizer V</a>&ThinSpace;
<span></span>
<span></span>, <a href="#"></a>
</Typography.Body>
<div class="h-7" />
<Typography.Headline class="mx-4" variant="medium">
</Typography.Headline>
<div class="mt-3 mx-1">
<Staff num={1} name="洛凛" role="策划、作词" />
<Staff num={2} name="鱼柳" role="作曲、编曲" />
<Staff num={3} name="月华" role="混音" />
<Staff num={4} name="城西阿灵" role="视频" />
<Staff num={5} name="与嬴酌棠" role="题字" />
</div>
</article>
</>
);
};

View File

@ -0,0 +1,51 @@
import { Component } from "solid-js";
import { A } from "@solidjs/router";
import { Button } from "@m3-components/solid";
import { HomeIcon, MusicIcon } from "~/components/icons";
import { StarBadge4, StarBadge6, StarBadge8 } from "~/components/icons/StarBadges";
import { HistoryIcon } from "../icons/History";
export const LeftSideBar: Component = () => {
return (
<>
<div class="inline-flex flex-col gap-4">
<A href="/">
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
<HomeIcon class="w-5 h-5 text-xl -translate-y-0.25" />
<span></span>
</Button>
</A>
<A href="/songs">
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
<MusicIcon class="w-5 h-5 text-xl" />
<span></span>
</Button>
</A>
<A href="/milestone/denndou/songs">
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
<StarBadge4 class="w-5 h-5 text-xl" />
<span>殿</span>
</Button>
</A>
<A href="/milestone/densetsu/songs">
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
<StarBadge6 class="w-5 h-5 text-xl" />
<span></span>
</Button>
</A>
<A href="/milestone/shinwa/songs">
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
<StarBadge8 class="w-5 h-5 text-xl" />
<span></span>
</Button>
</A>
<A href="../history">
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
<HistoryIcon class="w-5 h-5 text-xl" />
<span></span>
</Button>
</A>
</div>
</>
);
};

View File

@ -0,0 +1,17 @@
import { Component } from "solid-js";
import { ExtendedFAB } from "@m3-components/solid";
import { EditIcon } from "~/components/icons";
import { TabSwitcher } from "~/components/song/TabSwitcher";
export const RightSideBar: Component = () => {
return (
<>
<div class="w-48 self-center 2xl:self-end flex justify-end mb-6">
<ExtendedFAB position="unset" size="small" elevation={false} text="编辑" color="primary">
<EditIcon />
</ExtendedFAB>
</div>
<TabSwitcher />
</>
);
};

View File

@ -0,0 +1,37 @@
import { Component } from "solid-js";
import { A } from "@solidjs/router";
import { IconButton, Typography } from "@m3-components/solid";
import { RightArrow } from "~/components/icons/Arrow";
export const Staff: Component<{ name: string; role: string; num: number }> = (props) => {
return (
<A
href={`/author/${props.name}/info`}
class="group rounded-[1.25rem] hover:bg-surface-container h-16 flex items-center
px-4 justify-between"
>
<div class="ml-2 flex gap-5 lg:gap-4 grow w-full">
<span
class="font-[IPSD] font-medium text-[2rem] text-on-surface-variant"
style="
-webkit-text-stroke: var(--md-sys-color-on-surface-variant);
-webkit-text-stroke-width: 1.2px;
-webkit-text-fill-color: transparent;"
>
{props.num}
</span>
<div class="flex flex-col gap-[3px]">
<Typography.Body variant="large" class="text-on-surface font-medium">
{props.name}
</Typography.Body>
<Typography.Label variant="large" class="text-on-surface-variant">
{props.role}
</Typography.Label>
</div>
</div>
<IconButton class="text-on-surface-variant opacity-0 group-hover:opacity-80 duration-200">
<RightArrow />
</IconButton>
</A>
);
};

View File

@ -0,0 +1,40 @@
import { Button } from "@m3-components/solid";
import { A } from "@solidjs/router";
import { Component, splitProps } from "solid-js";
import { ElementProps } from "../common";
export const TabSwitcher: Component<ElementProps> = (props) => {
const [_v, rest] = splitProps(props, ["class"]);
return (
<nav class="flex flex-col" {...rest}>
<div class="w-full lg:w-48 gap-4 flex overflow-auto lg:flex-col items-center lg:self-center 2xl:self-end">
<A class="min-w-20 w-full" href="../info">
<Button class="w-full" variant="filled">
</Button>
</A>
<A class="min-w-20 w-full" href="../lyrics">
<Button class="w-full" variant="outlined">
</Button>
</A>
<A class="min-w-20 w-full" href="../analytics">
<Button class="w-full" variant="outlined">
</Button>
</A>
<A class="min-w-20 w-full" href="../relations">
<Button class="w-full" variant="outlined">
</Button>
</A>
<A class="min-w-20 w-full" href="../discussion">
<Button class="w-full" variant="outlined">
</Button>
</A>
</div>
</nav>
);
};

View File

@ -0,0 +1,22 @@
import { createPrefersDark } from "@solid-primitives/media";
import { Component, JSX, Match, splitProps, Switch } from "solid-js";
interface Props extends JSX.ImgHTMLAttributes<HTMLImageElement> {
lightSrc: string;
darkSrc: string;
}
export const DynamicImage: Component<Props> = (props) => {
const isDark = createPrefersDark();
const [v, rest] = splitProps(props, ["lightSrc", "darkSrc", "alt"]);
return (
<Switch>
<Match when={isDark()}>
<img src={v.darkSrc} alt={v.alt} {...rest} />
</Match>
<Match when={!isDark()}>
<img src={v.lightSrc} alt={v.alt} {...rest} />
</Match>
</Switch>
);
};

View File

@ -0,0 +1,44 @@
-- Current sql file was generated after introspecting the database
-- If you want to run this migration please uncomment this code before executing migrations
/*
CREATE SEQUENCE "public"."captcha_difficulty_settings_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1;--> statement-breakpoint
CREATE SEQUENCE "public"."users_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1;--> statement-breakpoint
CREATE TABLE "captcha_difficulty_settings" (
"id" integer DEFAULT nextval('captcha_difficulty_settings_id_seq'::regclass) NOT NULL,
"method" text NOT NULL,
"path" text NOT NULL,
"duration" real NOT NULL,
"threshold" integer NOT NULL,
"difficulty" integer NOT NULL,
"global" boolean NOT NULL
);
--> statement-breakpoint
CREATE TABLE "login_sessions" (
"id" text NOT NULL,
"uid" integer NOT NULL,
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
"expire_at" timestamp with time zone,
"last_used_at" timestamp with time zone,
"ip_address" "inet",
"user_agent" text,
"deactivated_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" integer DEFAULT nextval('users_id_seq'::regclass) NOT NULL,
"nickname" text,
"username" text NOT NULL,
"password" text NOT NULL,
"unq_id" text DEFAULT gen_random_uuid() NOT NULL,
"role" text DEFAULT 'USER' NOT NULL,
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX "captcha_difficulty_settings_pkey" ON "captcha_difficulty_settings" USING btree ("id" int4_ops);--> statement-breakpoint
CREATE INDEX "inx_login-sessions_uid" ON "login_sessions" USING btree ("uid" int4_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "login_sessions_pkey" ON "login_sessions" USING btree ("id" text_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "users_pkey" ON "users" USING btree ("id" int4_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "users_pkey1" ON "users" USING btree ("id" int4_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "users_unq_id_key" ON "users" USING btree ("unq_id" text_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "users_username_key" ON "users" USING btree ("username" text_ops);
*/

View File

@ -0,0 +1,350 @@
{
"id": "00000000-0000-0000-0000-000000000000",
"prevId": "",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.captcha_difficulty_settings": {
"name": "captcha_difficulty_settings",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": "nextval('captcha_difficulty_settings_id_seq'::regclass)"
},
"method": {
"name": "method",
"type": "text",
"primaryKey": false,
"notNull": true
},
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": true
},
"duration": {
"name": "duration",
"type": "real",
"primaryKey": false,
"notNull": true
},
"threshold": {
"name": "threshold",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"difficulty": {
"name": "difficulty",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"global": {
"name": "global",
"type": "boolean",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"captcha_difficulty_settings_pkey": {
"name": "captcha_difficulty_settings_pkey",
"columns": [
{
"expression": "id",
"asc": true,
"nulls": "last",
"opclass": "int4_ops",
"isExpression": false
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {},
"policies": {},
"isRLSEnabled": false
},
"public.login_sessions": {
"name": "login_sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"uid": {
"name": "uid",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "CURRENT_TIMESTAMP"
},
"expire_at": {
"name": "expire_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"last_used_at": {
"name": "last_used_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"ip_address": {
"name": "ip_address",
"type": "inet",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"deactivated_at": {
"name": "deactivated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"inx_login-sessions_uid": {
"name": "inx_login-sessions_uid",
"columns": [
{
"expression": "uid",
"asc": true,
"nulls": "last",
"opclass": "int4_ops",
"isExpression": false
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"login_sessions_pkey": {
"name": "login_sessions_pkey",
"columns": [
{
"expression": "id",
"asc": true,
"nulls": "last",
"opclass": "text_ops",
"isExpression": false
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {},
"policies": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": "nextval('users_id_seq'::regclass)"
},
"nickname": {
"name": "nickname",
"type": "text",
"primaryKey": false,
"notNull": false
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": true
},
"unq_id": {
"name": "unq_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'USER'"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "CURRENT_TIMESTAMP"
}
},
"indexes": {
"users_pkey": {
"name": "users_pkey",
"columns": [
{
"expression": "id",
"asc": true,
"nulls": "last",
"opclass": "int4_ops",
"isExpression": false
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"users_pkey1": {
"name": "users_pkey1",
"columns": [
{
"expression": "id",
"asc": true,
"nulls": "last",
"opclass": "int4_ops",
"isExpression": false
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"users_unq_id_key": {
"name": "users_unq_id_key",
"columns": [
{
"expression": "unq_id",
"asc": true,
"nulls": "last",
"opclass": "text_ops",
"isExpression": false
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"users_username_key": {
"name": "users_username_key",
"columns": [
{
"expression": "username",
"asc": true,
"nulls": "last",
"opclass": "text_ops",
"isExpression": false
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {},
"policies": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {
"public.captcha_difficulty_settings_id_seq": {
"name": "captcha_difficulty_settings_id_seq",
"schema": "public",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"increment": "1",
"cycle": false,
"cache": "1"
},
"public.users_id_seq": {
"name": "users_id_seq",
"schema": "public",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"increment": "1",
"cycle": false,
"cache": "1"
}
},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"tables": {
"captcha_difficulty_settings": {
"columns": {
"id": {
"isDefaultAnExpression": true
}
}
},
"users": {
"columns": {
"id": {
"isDefaultAnExpression": true
}
}
}
}
}
}

View File

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1750513073792,
"tag": "0000_moaning_shotgun",
"breakpoints": true
}
]
}

View File

@ -0,0 +1,2 @@
import { relations } from "drizzle-orm/relations";
import {} from "./schema";

View File

@ -0,0 +1,93 @@
import {
pgTable,
uniqueIndex,
integer,
text,
real,
boolean,
index,
timestamp,
inet,
pgSequence
} from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
export const captchaDifficultySettingsIdSeq = pgSequence("captcha_difficulty_settings_id_seq", {
startWith: "1",
increment: "1",
minValue: "1",
maxValue: "2147483647",
cache: "1",
cycle: false
});
export const usersIdSeq = pgSequence("users_id_seq", {
startWith: "1",
increment: "1",
minValue: "1",
maxValue: "2147483647",
cache: "1",
cycle: false
});
export const captchaDifficultySettings = pgTable(
"captcha_difficulty_settings",
{
id: integer()
.default(sql`nextval('captcha_difficulty_settings_id_seq'::regclass)`)
.notNull(),
method: text().notNull(),
path: text().notNull(),
duration: real().notNull(),
threshold: integer().notNull(),
difficulty: integer().notNull(),
global: boolean().notNull()
},
(table) => [
uniqueIndex("captcha_difficulty_settings_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops"))
]
);
export const loginSessions = pgTable(
"login_sessions",
{
id: text().notNull(),
uid: integer().notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
expireAt: timestamp("expire_at", { withTimezone: true, mode: "string" }),
lastUsedAt: timestamp("last_used_at", { withTimezone: true, mode: "string" }),
ipAddress: inet("ip_address"),
userAgent: text("user_agent"),
deactivatedAt: timestamp("deactivated_at", { withTimezone: true, mode: "string" })
},
(table) => [
index("inx_login-sessions_uid").using("btree", table.uid.asc().nullsLast().op("int4_ops")),
uniqueIndex("login_sessions_pkey").using("btree", table.id.asc().nullsLast().op("text_ops"))
]
);
export const users = pgTable(
"users",
{
id: integer()
.default(sql`nextval('users_id_seq'::regclass)`)
.notNull(),
nickname: text(),
username: text().notNull(),
password: text().notNull(),
unqId: text("unq_id")
.default(sql`gen_random_uuid()`)
.notNull(),
role: text().default("USER").notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
.default(sql`CURRENT_TIMESTAMP`)
.notNull()
},
(table) => [
uniqueIndex("users_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
uniqueIndex("users_pkey1").using("btree", table.id.asc().nullsLast().op("int4_ops")),
uniqueIndex("users_unq_id_key").using("btree", table.unqId.asc().nullsLast().op("text_ops")),
uniqueIndex("users_username_key").using("btree", table.username.asc().nullsLast().op("text_ops"))
]
);

View File

@ -0,0 +1,7 @@
"use server";
import { drizzle } from "drizzle-orm/postgres-js";
import { sqlCred, sql } from "@cvsa/core";
export const dbMain = drizzle(sql);
export const dbCred = drizzle(sqlCred);

View File

@ -0,0 +1,157 @@
-- Current sql file was generated after introspecting the database
-- If you want to run this migration please uncomment this code before executing migrations
/*
CREATE SEQUENCE "public"."all_data_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1;--> statement-breakpoint
CREATE SEQUENCE "public"."labeling_result_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1;--> statement-breakpoint
CREATE SEQUENCE "public"."songs_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1;--> statement-breakpoint
CREATE SEQUENCE "public"."video_snapshot_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1;--> statement-breakpoint
CREATE SEQUENCE "public"."views_increment_rate_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1;--> statement-breakpoint
CREATE TABLE "content" (
"page_id" text PRIMARY KEY NOT NULL,
"page_content" text NOT NULL,
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updated_at" timestamp with time zone,
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "bilibili_user" (
"id" serial PRIMARY KEY NOT NULL,
"uid" bigint NOT NULL,
"username" text NOT NULL,
"desc" text NOT NULL,
"fans" integer NOT NULL,
CONSTRAINT "unq_bili-user_uid" UNIQUE("uid")
);
--> statement-breakpoint
CREATE TABLE "bilibili_metadata" (
"id" integer DEFAULT nextval('all_data_id_seq'::regclass) NOT NULL,
"aid" bigint NOT NULL,
"bvid" varchar(12),
"description" text,
"uid" bigint,
"tags" text,
"title" text,
"published_at" timestamp with time zone,
"duration" integer,
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
"status" integer DEFAULT 0 NOT NULL,
"cover_url" text
);
--> statement-breakpoint
CREATE TABLE "classified_labels_human" (
"id" serial PRIMARY KEY NOT NULL,
"aid" bigint NOT NULL,
"author" uuid NOT NULL,
"label" smallint NOT NULL,
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
);
--> statement-breakpoint
CREATE TABLE "labelling_result" (
"id" integer DEFAULT nextval('labeling_result_id_seq'::regclass) NOT NULL,
"aid" bigint NOT NULL,
"label" smallint NOT NULL,
"model_version" text NOT NULL,
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
"logits" smallint[]
);
--> statement-breakpoint
CREATE TABLE "latest_video_snapshot" (
"aid" bigint PRIMARY KEY NOT NULL,
"time" timestamp with time zone NOT NULL,
"views" integer NOT NULL,
"coins" integer NOT NULL,
"likes" integer NOT NULL,
"favorites" integer NOT NULL,
"replies" integer NOT NULL,
"danmakus" integer NOT NULL,
"shares" integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE "video_snapshot" (
"id" integer DEFAULT nextval('video_snapshot_id_seq'::regclass) NOT NULL,
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
"views" integer NOT NULL,
"coins" integer NOT NULL,
"likes" integer NOT NULL,
"favorites" integer NOT NULL,
"shares" integer NOT NULL,
"danmakus" integer NOT NULL,
"aid" bigint NOT NULL,
"replies" integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE "snapshot_schedule" (
"id" bigserial NOT NULL,
"aid" bigint NOT NULL,
"type" text,
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
"started_at" timestamp with time zone,
"finished_at" timestamp with time zone,
"status" text DEFAULT 'pending' NOT NULL
);
--> statement-breakpoint
CREATE TABLE "songs" (
"id" integer DEFAULT nextval('songs_id_seq'::regclass) NOT NULL,
"name" text,
"aid" bigint,
"published_at" timestamp with time zone,
"duration" integer,
"type" smallint,
"romanized_name" text,
"netease_id" bigint,
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
"deleted" boolean DEFAULT false NOT NULL
);
--> statement-breakpoint
CREATE TABLE "views_increment_rate" (
"id" integer DEFAULT nextval('views_increment_rate_id_seq'::regclass) NOT NULL,
"aid" bigint NOT NULL,
"old_time" timestamp with time zone NOT NULL,
"new_time" timestamp with time zone NOT NULL,
"old_views" integer NOT NULL,
"new_views" integer NOT NULL,
"interval" interval NOT NULL,
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
"speed" real
);
--> statement-breakpoint
CREATE INDEX "idx_content_created-at" ON "content" USING btree ("created_at" timestamptz_ops);--> statement-breakpoint
CREATE INDEX "idx_bili-user_uid" ON "bilibili_user" USING btree ("uid" int8_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "all_data_pkey" ON "bilibili_metadata" USING btree ("id" int4_ops);--> statement-breakpoint
CREATE INDEX "idx_all-data_aid" ON "bilibili_metadata" USING btree ("aid" int8_ops);--> statement-breakpoint
CREATE INDEX "idx_all-data_bvid" ON "bilibili_metadata" USING btree ("bvid" text_ops);--> statement-breakpoint
CREATE INDEX "idx_all-data_uid" ON "bilibili_metadata" USING btree ("uid" int8_ops);--> statement-breakpoint
CREATE INDEX "idx_bili-meta_status" ON "bilibili_metadata" USING btree ("status" int4_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "unq_all-data_aid" ON "bilibili_metadata" USING btree ("aid" int8_ops);--> statement-breakpoint
CREATE INDEX "idx_classified-labels-human_aid" ON "classified_labels_human" USING btree ("aid" int8_ops);--> statement-breakpoint
CREATE INDEX "idx_classified-labels-human_author" ON "classified_labels_human" USING btree ("author" uuid_ops);--> statement-breakpoint
CREATE INDEX "idx_classified-labels-human_created-at" ON "classified_labels_human" USING btree ("created_at" timestamptz_ops);--> statement-breakpoint
CREATE INDEX "idx_classified-labels-human_label" ON "classified_labels_human" USING btree ("label" int2_ops);--> statement-breakpoint
CREATE INDEX "idx_labeling_label_model-version" ON "labelling_result" USING btree ("label" int2_ops,"model_version" int2_ops);--> statement-breakpoint
CREATE INDEX "idx_labeling_model-version" ON "labelling_result" USING btree ("model_version" text_ops);--> statement-breakpoint
CREATE INDEX "idx_labelling_aid-label" ON "labelling_result" USING btree ("aid" int2_ops,"label" int2_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "labeling_result_pkey" ON "labelling_result" USING btree ("id" int4_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "unq_labelling-result_aid_model-version" ON "labelling_result" USING btree ("aid" int8_ops,"model_version" int8_ops);--> statement-breakpoint
CREATE INDEX "idx_latest-video-snapshot_time" ON "latest_video_snapshot" USING btree ("time" timestamptz_ops);--> statement-breakpoint
CREATE INDEX "idx_latest-video-snapshot_views" ON "latest_video_snapshot" USING btree ("views" int4_ops);--> statement-breakpoint
CREATE INDEX "idx_vid_snapshot_aid" ON "video_snapshot" USING btree ("aid" int8_ops);--> statement-breakpoint
CREATE INDEX "idx_vid_snapshot_time" ON "video_snapshot" USING btree ("created_at" timestamptz_ops);--> statement-breakpoint
CREATE INDEX "idx_vid_snapshot_views" ON "video_snapshot" USING btree ("views" int4_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "video_snapshot_pkey" ON "video_snapshot" USING btree ("id" int4_ops);--> statement-breakpoint
CREATE INDEX "idx_snapshot_schedule_aid" ON "snapshot_schedule" USING btree ("aid" int8_ops);--> statement-breakpoint
CREATE INDEX "idx_snapshot_schedule_started_at" ON "snapshot_schedule" USING btree ("started_at" timestamptz_ops);--> statement-breakpoint
CREATE INDEX "idx_snapshot_schedule_status" ON "snapshot_schedule" USING btree ("status" text_ops);--> statement-breakpoint
CREATE INDEX "idx_snapshot_schedule_type" ON "snapshot_schedule" USING btree ("type" text_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "snapshot_schedule_pkey" ON "snapshot_schedule" USING btree ("id" int8_ops);--> statement-breakpoint
CREATE INDEX "idx_aid" ON "songs" USING btree ("aid" int8_ops);--> statement-breakpoint
CREATE INDEX "idx_hash_songs_aid" ON "songs" USING hash ("aid" int8_ops);--> statement-breakpoint
CREATE INDEX "idx_netease_id" ON "songs" USING btree ("netease_id" int8_ops);--> statement-breakpoint
CREATE INDEX "idx_published_at" ON "songs" USING btree ("published_at" timestamptz_ops);--> statement-breakpoint
CREATE INDEX "idx_type" ON "songs" USING btree ("type" int2_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "songs_pkey" ON "songs" USING btree ("id" int4_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "unq_songs_aid" ON "songs" USING btree ("aid" int8_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "unq_songs_netease_id" ON "songs" USING btree ("netease_id" int8_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "unq_views-increment-rate_aid_interval" ON "views_increment_rate" USING btree ("aid" int8_ops,"interval" int8_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "views_increment_rate_pkey" ON "views_increment_rate" USING btree ("id" int4_ops);
*/

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1750513105905,
"tag": "0000_fresh_mac_gargan",
"breakpoints": true
}
]
}

View File

@ -0,0 +1,3 @@
import { relations } from "drizzle-orm/relations";
import { } from "./schema";

View File

@ -0,0 +1,174 @@
import { pgTable, uniqueIndex, index, integer, bigint, varchar, text, timestamp, unique, serial, smallint, boolean, bigserial, uuid, pgSequence } from "drizzle-orm/pg-core"
import { sql } from "drizzle-orm"
export const allDataIdSeq = pgSequence("all_data_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "2147483647", cache: "1", cycle: false })
export const labelingResultIdSeq = pgSequence("labeling_result_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "2147483647", cache: "1", cycle: false })
export const songsIdSeq = pgSequence("songs_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "2147483647", cache: "1", cycle: false })
export const videoSnapshotIdSeq = pgSequence("video_snapshot_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "2147483647", cache: "1", cycle: false })
export const viewsIncrementRateIdSeq = pgSequence("views_increment_rate_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "9223372036854775807", cache: "1", cycle: false })
export const bilibiliMetadata = pgTable("bilibili_metadata", {
id: integer().default(sql`nextval('all_data_id_seq'::regclass)`).notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
aid: bigint({ mode: "number" }).notNull(),
bvid: varchar({ length: 12 }),
description: text(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
uid: bigint({ mode: "number" }),
tags: text(),
title: text(),
publishedAt: timestamp("published_at", { withTimezone: true, mode: 'string' }),
duration: integer(),
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`),
status: integer().default(0).notNull(),
coverUrl: text("cover_url"),
}, (table) => [
uniqueIndex("all_data_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
index("idx_all-data_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
index("idx_all-data_bvid").using("btree", table.bvid.asc().nullsLast().op("text_ops")),
index("idx_all-data_uid").using("btree", table.uid.asc().nullsLast().op("int8_ops")),
index("idx_bili-meta_status").using("btree", table.status.asc().nullsLast().op("int4_ops")),
uniqueIndex("unq_all-data_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
]);
export const bilibiliUser = pgTable("bilibili_user", {
id: serial().primaryKey().notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
uid: bigint({ mode: "number" }).notNull(),
username: text().notNull(),
desc: text().notNull(),
fans: integer().notNull(),
}, (table) => [
index("idx_bili-user_uid").using("btree", table.uid.asc().nullsLast().op("int8_ops")),
unique("unq_bili-user_uid").on(table.uid),
]);
export const labellingResult = pgTable("labelling_result", {
id: integer().default(sql`nextval('labeling_result_id_seq'::regclass)`).notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
aid: bigint({ mode: "number" }).notNull(),
label: smallint().notNull(),
modelVersion: text("model_version").notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
logits: smallint().array(),
}, (table) => [
index("idx_labeling_label_model-version").using("btree", table.label.asc().nullsLast().op("int2_ops"), table.modelVersion.asc().nullsLast().op("int2_ops")),
index("idx_labeling_model-version").using("btree", table.modelVersion.asc().nullsLast().op("text_ops")),
index("idx_labelling_aid-label").using("btree", table.aid.asc().nullsLast().op("int2_ops"), table.label.asc().nullsLast().op("int2_ops")),
uniqueIndex("labeling_result_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
uniqueIndex("unq_labelling-result_aid_model-version").using("btree", table.aid.asc().nullsLast().op("int8_ops"), table.modelVersion.asc().nullsLast().op("int8_ops")),
]);
export const latestVideoSnapshot = pgTable("latest_video_snapshot", {
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
aid: bigint({ mode: "number" }).primaryKey().notNull(),
time: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
views: integer().notNull(),
coins: integer().notNull(),
likes: integer().notNull(),
favorites: integer().notNull(),
replies: integer().notNull(),
danmakus: integer().notNull(),
shares: integer().notNull(),
}, (table) => [
index("idx_latest-video-snapshot_time").using("btree", table.time.asc().nullsLast().op("timestamptz_ops")),
index("idx_latest-video-snapshot_views").using("btree", table.views.asc().nullsLast().op("int4_ops")),
]);
export const videoSnapshot = pgTable("video_snapshot", {
id: integer().default(sql`nextval('video_snapshot_id_seq'::regclass)`).notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
views: integer().notNull(),
coins: integer().notNull(),
likes: integer().notNull(),
favorites: integer().notNull(),
shares: integer().notNull(),
danmakus: integer().notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
aid: bigint({ mode: "number" }).notNull(),
replies: integer().notNull(),
}, (table) => [
index("idx_vid_snapshot_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
index("idx_vid_snapshot_aid_created_at").using("btree", table.aid.asc().nullsLast().op("timestamptz_ops"), table.createdAt.asc().nullsLast().op("timestamptz_ops")),
index("idx_vid_snapshot_time").using("btree", table.createdAt.asc().nullsLast().op("timestamptz_ops")),
index("idx_vid_snapshot_views").using("btree", table.views.asc().nullsLast().op("int4_ops")),
uniqueIndex("video_snapshot_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
]);
export const songs = pgTable("songs", {
id: integer().default(sql`nextval('songs_id_seq'::regclass)`).notNull(),
name: text(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
aid: bigint({ mode: "number" }),
publishedAt: timestamp("published_at", { withTimezone: true, mode: 'string' }),
duration: integer(),
type: smallint(),
romanizedName: text("romanized_name"),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
neteaseId: bigint("netease_id", { mode: "number" }),
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
deleted: boolean().default(false).notNull(),
image: text(),
}, (table) => [
index("idx_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
index("idx_hash_songs_aid").using("hash", table.aid.asc().nullsLast().op("int8_ops")),
index("idx_netease_id").using("btree", table.neteaseId.asc().nullsLast().op("int8_ops")),
index("idx_published_at").using("btree", table.publishedAt.asc().nullsLast().op("timestamptz_ops")),
index("idx_type").using("btree", table.type.asc().nullsLast().op("int2_ops")),
uniqueIndex("songs_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
uniqueIndex("unq_songs_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
uniqueIndex("unq_songs_netease_id").using("btree", table.neteaseId.asc().nullsLast().op("int8_ops")),
]);
export const singer = pgTable("singer", {
id: serial().primaryKey().notNull(),
name: text().notNull(),
});
export const relations = pgTable("relations", {
id: serial().primaryKey().notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
sourceId: bigint("source_id", { mode: "number" }).notNull(),
sourceType: text("source_type").notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
targetId: bigint("target_id", { mode: "number" }).notNull(),
targetType: text("target_type").notNull(),
relation: text().notNull(),
}, (table) => [
index("idx_relations_source_id_source_type_relation").using("btree", table.sourceId.asc().nullsLast().op("int8_ops"), table.sourceType.asc().nullsLast().op("int8_ops"), table.relation.asc().nullsLast().op("text_ops")),
index("idx_relations_target_id_target_type_relation").using("btree", table.targetId.asc().nullsLast().op("text_ops"), table.targetType.asc().nullsLast().op("text_ops"), table.relation.asc().nullsLast().op("text_ops")),
unique("unq_relations").on(table.sourceId, table.sourceType, table.targetId, table.targetType, table.relation),
]);
export const snapshotSchedule = pgTable("snapshot_schedule", {
id: bigserial({ mode: "bigint" }).notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
aid: bigint({ mode: "number" }).notNull(),
type: text(),
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
startedAt: timestamp("started_at", { withTimezone: true, mode: 'string' }),
finishedAt: timestamp("finished_at", { withTimezone: true, mode: 'string' }),
status: text().default('pending').notNull(),
}, (table) => [
index("idx_snapshot_schedule_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
index("idx_snapshot_schedule_started_at").using("btree", table.startedAt.asc().nullsLast().op("timestamptz_ops")),
index("idx_snapshot_schedule_status").using("btree", table.status.asc().nullsLast().op("text_ops")),
index("idx_snapshot_schedule_type").using("btree", table.type.asc().nullsLast().op("text_ops")),
uniqueIndex("snapshot_schedule_pkey").using("btree", table.id.asc().nullsLast().op("int8_ops")),
]);
export const classifiedLabelsHuman = pgTable("classified_labels_human", {
id: serial().primaryKey().notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
aid: bigint({ mode: "number" }).notNull(),
author: uuid().notNull(),
label: smallint().notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
}, (table) => [
index("idx_classified-labels-human_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
index("idx_classified-labels-human_author").using("btree", table.author.asc().nullsLast().op("uuid_ops")),
index("idx_classified-labels-human_created-at").using("btree", table.createdAt.asc().nullsLast().op("timestamptz_ops")),
index("idx_classified-labels-human_label").using("btree", table.label.asc().nullsLast().op("int2_ops")),
]);

View File

@ -0,0 +1,10 @@
import type { InferSelectModel } from "drizzle-orm";
import { users } from "~db/cred/schema";
import { bilibiliMetadata, latestVideoSnapshot, songs, videoSnapshot } from "~db/main/schema";
export type UserType = InferSelectModel<typeof users>;
export type SensitiveUserFields = "password" | "unqId";
export type BilibiliMetadataType = InferSelectModel<typeof bilibiliMetadata>;
export type VideoSnapshotType = InferSelectModel<typeof videoSnapshot>;
export type LatestVideoSnapshotType = InferSelectModel<typeof latestVideoSnapshot>;
export type SongType = InferSelectModel<typeof songs>;

View File

@ -0,0 +1,15 @@
// @refresh reload
import { mount, StartClient } from "@solidjs/start/client";
import { RequestContextProvider } from "./components/requestContext";
import { MetaProvider } from "@solidjs/meta";
mount(
() => (
<RequestContextProvider>
<MetaProvider>
<StartClient />
</MetaProvider>
</RequestContextProvider>
),
document.getElementById("app")!
);

View File

@ -0,0 +1,29 @@
// @refresh reload
import { createHandler, StartServer } from "@solidjs/start/server";
import { MetaProvider } from "@solidjs/meta";
import { RequestContextProvider } from "~/components/requestContext";
export default createHandler(() => (
<RequestContextProvider>
<MetaProvider>
<StartServer
document={({ assets, children, scripts }) => (
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{assets}
</head>
<body>
<div id="app">
{children}
</div>
<div id="modal"></div>
{scripts}
</body>
</html>
)}
/>
</MetaProvider>
</RequestContextProvider>
));

1
packages/solid/src/global.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="@solidjs/start/env" />

View File

@ -0,0 +1,25 @@
const N_1024 = BigInt(
"129023318876534346704360951712586568674758913224876821534686030409476129469193481910786173836188085930974906857867802234113909470848523288588793477904039083513378341278558405407018889387577114155572311708428733260891448259786041525189132461448841652472631435226032063278124857443496954605482776113964107326943"
);
const N_2048 = BigInt(
"23987552118069940970878653610463005981599204778388399885550631951871084945075866571231062435627294546200946516668493107358732376187241747090707087544153108117326163500579370560400058549184722138636116585329496684877258304519458316233517215780035360354808658620079068489084797380781488445517430961701007542207001544091884001098497324624368085682074645221148086075871342544591022944384890014176612259729018968864426602901247715051556212559854689574013699665035317257438297910516976812428036717668766321871780963854649899276251822244719887233041422346429752896925499321431273560130952088238625622570366815755926694833109"
);
const N_1792 = BigInt(
"23987552118069940970878653610463005981599204778388399885550631951871084945075866571231062435627294546200946516668493107358732376187241747090707087544153108117326163500579370560400058549184722138636116585329496684877258304519458316233517215780035360354808658620079068489084797380781488445517430961701007542207001544091884001098497324624368085682074645221148086075871342544591022944384890014176612259729018968864426602901247715051556212559854689574013699665035317257438297910516976812428036717668766321871780963854649899276251822244719887233041422346429752896925499321431273560130952088238625622570366815755926694833109"
);
const N_1536 = BigInt(
"1694330250214463438908848400950857073137355630337290254958754184668036770489801447652464038218330711288158361242955860326168191830448553710492926795708495297280933502917598985378231124113971732841791156356676046934277122699383776036675381503510992810963611269045078440132744168908318454891211962146563551929591147663448816841024591820348784855441153716551049843185172472891407933214238000452095646085222944171689449292644270516031799660928056315886939284985905227"
);
const N_3072 = BigInt(
"4432919939296042464443862503456460073874727648022810391370558006281079088795179408238989283371442564716849343712703672836423961818025813387453469700639513190304802553045342607888612037304066433501317127429264242784608682213025490491212489901736408833027611579294436675682774458141490718959615677971745638214649336218217578937534746160749039668886450447773018369168258067682196337978245372237157696236362344796867228581553446331915147012787367438751646936429739232247148712001806846526947508445039707404287951727838234648917450736371192435665040644040487427986702098273581288935278964444790007953559851323281510927332862225214878776790605026472021669614552481167977412450477230442015077669503312683966631454347169703030544483487968842349634064181183599641180349414682042575010303056241481622837185325228233789954078775053744988023738762706404546546146837242590884760044438874357295029411988267287001033032827035809135092270843"
);
const N_4096 = BigInt(
"703671044356805218391078271512201582198770553281951369783674142891088501340774249238173262580562112786670043634665390581120113644316651934154746357220932310140476300088580654571796404198410555061275065442553506658401183560336140989074165998202690496991174269748740565700402715364422506782445179963440819952745241176450402011121226863984008975377353558155910994380700267903933205531681076494639818328879475919332604951949178075254600102192323286738973253864238076198710173840170988339024438220034106150475640983877458155141500313471699516670799821379238743709125064098477109094533426340852518505385314780319279862586851512004686798362431227795743253799490998475141728082088984359237540124375439664236138519644100625154580910233437864328111620708697941949936338367445851449766581651338876219676721272448769082914348242483068204896479076062102236087066428603930888978596966798402915747531679758905013008059396214343112694563043918465373870648649652122703709658068801764236979191262744515840224548957285182453209028157886219424802426566456408109642062498413592155064289314088837031184200671561102160059065729282902863248815224399131391716503171191977463328439766546574118092303414702384104112719959325482439604572518549918705623086363111"
);
export const N_ARRAY = [N_1024, N_1536, N_1792, N_2048, N_3072, N_4096];

View File

@ -0,0 +1,32 @@
import { Context } from "~/components/requestContext";
export async function useCachedFetch<T>(
fetcher: () => Promise<T>,
identifier: string,
context: Context,
deps: any[]
): Promise<T> {
const [contextSignal, updateContext] = context;
const hooks = contextSignal();
let hook = hooks.get(identifier);
if (hook && hook.promise) {
return hook.promise;
}
hook = {
memoizedValue: null,
deps: deps,
promise: null
};
const promise = fetcher().then((result) => {
hook!.memoizedValue = result;
hooks.set(identifier, hook!);
updateContext(hooks);
return result;
});
hook.promise = promise;
hooks.set(identifier, hook!);
updateContext(hooks);
return promise;
}

View File

@ -0,0 +1,70 @@
import axios, { AxiosRequestConfig, AxiosError, Method, AxiosResponse } from "axios";
export class ApiRequestError extends Error {
public code: number | undefined;
public response: unknown | undefined;
constructor(message: string, res?: unknown, code?: number) {
super(message);
this.name = "ApiRequestError";
this.code = code;
this.response = res;
}
}
type HttpMethod = Extract<Method, "GET" | "POST" | "PUT" | "DELETE" | "PATCH">;
const httpMethods = {
get: axios.get,
post: axios.post,
put: axios.put,
delete: axios.delete,
patch: axios.patch
};
export function fetcher(url: string): Promise<unknown>;
export function fetcher<JSON = unknown>(
url: string,
init?: Omit<AxiosRequestConfig, "method"> & { method?: Exclude<HttpMethod, "DELETE"> }
): Promise<JSON>;
export function fetcher(
url: string,
init: Omit<AxiosRequestConfig, "method"> & { method: "DELETE" }
): Promise<AxiosResponse>;
export async function fetcher<JSON = unknown>(
url: string,
init?: Omit<AxiosRequestConfig, "method"> & { method?: HttpMethod }
): Promise<JSON | AxiosResponse<any, any>> {
const { method = "get", data, ...config } = init || {};
const fullConfig: AxiosRequestConfig = {
method,
...config,
timeout: 10000
};
try {
const m = method.toLowerCase() as keyof typeof httpMethods;
if (["post", "patch", "put"].includes(m)) {
const response = await httpMethods[m](url, data, fullConfig);
return response.data;
} else if (m === "delete") {
const response = await axios.delete(url, fullConfig);
return response;
} else {
const response = await httpMethods[m](url, fullConfig);
return response.data;
}
} catch (error) {
const axiosError = error as AxiosError;
if (axiosError.response) {
const { status, data } = axiosError.response;
throw new ApiRequestError(`HTTP error! status: ${status}`, data, status);
} else if (axiosError.request) {
throw new ApiRequestError("No response received", undefined, -1);
} else {
throw new ApiRequestError(axiosError.message || "Unknown error");
}
}
}

View File

@ -0,0 +1,117 @@
// Define interfaces for input and output
interface VdfProgressCallback {
(progress: number): void;
}
interface VdfResult {
result: bigint;
time: number; // Time taken in milliseconds
}
// The content of the Web Worker script
const workerContent = `addEventListener("message", async (event) => {
const { g, N, difficulty } = event.data;
// Although pow is not used in the iterative VDF, it's good to keep the original worker code structure.
// The iterative computeVDFWithProgress is better for progress reporting.
function pow(base, exponent, mod) {
let result = 1n;
base = base % mod;
while (exponent > 0n) {
if (exponent % 2n === 1n) {
result = (result * base) % mod;
}
base = (base * base) % mod;
exponent = exponent / 2n;
// Using BigInt division (/) which performs integer division
}
return result;
}
// Compute VDF iteratively to report progress
function computeVDFWithProgress(g, N, T, postProgress) {
let result = g;
let latestTime = performance.now();
const totalSteps = T; // T is the difficulty, representing 2^T squaring steps
for (let i = 0n; i < totalSteps; i++) {
result = (result * result) % N;
// Report progress periodically (approx. every 16ms to match typical frame rate)
if (performance.now() - latestTime > 16) {
// Calculate progress as a percentage
const progress = Number((i + 1n) * 10000n / totalSteps) / 100; // Using 10000 for better precision before dividing by 100
postProgress(progress);
latestTime = performance.now();
}
}
// Ensure final progress is reported
postProgress(100);
return result;
}
const startTime = performance.now();
// The worker computes g^(2^difficulty) mod N. The loop runs 'difficulty' times, performing squaring.
const result = computeVDFWithProgress(g, N, difficulty, (progress) => {
// Post progress back to the main thread
postMessage({ type: "progress", progress: progress });
});
const endTime = performance.now();
const timeTaken = endTime - startTime;
// Post the final result and time taken back to the main thread
postMessage({ type: "result", result: result.toString(), time: timeTaken });
});
`;
/**
* Computes the Verifiable Delay Function (VDF) result g^(2^difficulty) mod N
* in a Web Worker and reports progress.
* @param g - The base (bigint).
* @param N - The modulus (bigint).
* @param difficulty - The number of squaring steps (T) (bigint).
* @param onProgress - Optional callback function to receive progress updates (0-100).
* @returns A Promise that resolves with the VDF result and time taken.
*/
export function computeVdfInWorker(
g: bigint,
N: bigint,
difficulty: bigint,
onProgress?: VdfProgressCallback
): Promise<VdfResult> {
return new Promise((resolve, reject) => {
// Create a Blob containing the worker script
const blob = new Blob([workerContent], { type: "text/javascript" });
// Create a URL for the Blob
const workerUrl = URL.createObjectURL(blob);
// Create a new Web Worker
const worker = new window.Worker(workerUrl);
// Handle messages from the worker
worker.onmessage = (event) => {
const { type, progress, result, time } = event.data;
if (type === "progress") {
if (onProgress) {
onProgress(progress);
}
} else if (type === "result") {
// Resolve the promise with the result and time
resolve({ result: BigInt(result), time });
// Terminate the worker and revoke the URL
worker.terminate();
URL.revokeObjectURL(workerUrl);
}
};
// Handle potential errors in the worker
worker.onerror = (error) => {
reject(error);
// Terminate the worker and revoke the URL in case of error
worker.terminate();
URL.revokeObjectURL(workerUrl);
};
// Post the data to the worker to start the computation
worker.postMessage({ g, N, difficulty });
});
}

View File

@ -0,0 +1,36 @@
import { plugin } from "postcss";
export default plugin("postcss-calc-keyword-polyfill", () => {
const replacements = {
pi: "3.141592653589793",
e: "2.718281828459045",
infinity: "1e308", // A very large number to simulate infinity
"-infinity": "-1e308", // A very small number to simulate -infinity
nan: "0/0" // Division by zero in calc() results in NaN in modern browsers
};
// Regex to find the keywords, case-insensitive
const keywordRegex = new RegExp(`\\b(-?(${Object.keys(replacements).join("|")}))\\b`, "gi");
return (root) => {
root.walkDecls((decl) => {
// Check if the declaration value contains calc()
if (decl.value.toLowerCase().includes("calc(")) {
decl.value = decl.value.replace(/calc\(([^)]+)\)/gi, (match, expression) => {
const newExpression = expression.replace(keywordRegex, (keyword) => {
const lowerKeyword = keyword.toLowerCase();
if (lowerKeyword in replacements) {
return replacements[lowerKeyword];
}
// Handle cases like -pi and -e
if (lowerKeyword.startsWith("-") && lowerKeyword.substring(1) in replacements) {
return `-${replacements[lowerKeyword.substring(1)]}`;
}
return keyword; // Should not happen with the current regex, but as a fallback
});
return `calc(${newExpression})`;
});
}
});
};
});

View File

@ -0,0 +1,18 @@
import { Title } from "@solidjs/meta";
import { HttpStatusCode } from "@solidjs/start";
import { Layout } from "~/components/layout";
import { A } from "@solidjs/router";
export default function NotFound() {
return (
<Layout>
<Title></Title>
<HttpStatusCode code={404} />
<main class="w-full h-[calc(100vh-6rem)] flex flex-col flex-grow items-center justify-center gap-8">
<h1 class="text-9xl font-thin">404</h1>
<p class="text-xl font-medium">(Дд)!?</p>
<A href="/"></A>
</main>
</Layout>
);
}

View File

@ -0,0 +1,73 @@
import { Layout } from "~/components/layout";
import { query } from "@solidjs/router";
import { Card, CardContent, CardMedia, Typography } from "@m3-components/solid";
export default function Info() {
return (
<Layout lang="en">
<title></title>
<main class="w-full pt-14 lg:max-w-lg xl:max-w-xl lg:mx-auto">
<Card variant="outlined">
<CardMedia
round={false}
src="https://i0.hdslb.com/bfs/archive/8ad220336f96e4d2ea05baada3bc04592d56b2a5.jpg"
referrerpolicy="no-referrer"
/>
<CardContent>
<div class="flex flex-col">
<Typography.Headline variant="small"></Typography.Headline>
<Typography.Body class="font-medium text-on-surface-variant" variant="large">
Chen Hai Hui Xian Yuan
</Typography.Body>
</div>
<div class="mt-4 grid grid-cols-2 grid-rows-3 gap-2 ">
<div class="flex flex-col">
<Typography.Body class="font-semibold" variant="small">
PUBLISHER
</Typography.Body>
<Typography.Body variant="large"></Typography.Body>
</div>
<div class="flex flex-col">
<Typography.Body class="font-semibold" variant="small">
DURATION
</Typography.Body>
<Typography.Body variant="large">4:28</Typography.Body>
</div>
<div class="flex flex-col">
<Typography.Body class="font-semibold" variant="small">
SINGER
</Typography.Body>
<Typography.Body variant="large">
<a href="#"></a> <span class="text-on-surface-variant">(Chiyu)</span>
</Typography.Body>
</div>
<div class="flex flex-col">
<Typography.Body class="font-semibold" variant="small">
PUBLISH TIME
</Typography.Body>
<Typography.Body variant="large">2024-12-15 12:15:00</Typography.Body>
</div>
<div class="flex flex-col">
<Typography.Body class="font-semibold" variant="small">
VIEWS
</Typography.Body>
<Typography.Body variant="large">12.4K (12,422)</Typography.Body>
</div>
<div class="flex flex-col">
<Typography.Body class="font-semibold" variant="small">
LINKS
</Typography.Body>
<Typography.Body class="flex gap-2" variant="large">
<a href="https://www.bilibili.com/video/BV1eaq9Y3EVV/">bilibili</a>
<a href="https://vocadb.net/S/742394">VocaDB</a>
</Typography.Body>
</div>
</div>
</CardContent>
</Card>
</main>
</Layout>
);
}

View File

@ -0,0 +1,70 @@
import { Layout } from "~/components/layout";
import { dbMain } from "~/drizzle";
import { bilibiliMetadata, latestVideoSnapshot } from "~db/main/schema";
import { and, desc, eq, gte, lt } from "drizzle-orm";
import { createAsync, query, RouteDefinition } from "@solidjs/router";
import { Component, createResource, For, Suspense } from "solid-js";
import { BilibiliMetadataType, LatestVideoSnapshotType } from "~db/outerSchema";
import { Card, CardContent, CardMedia, Typography } from "@m3-components/solid";
const getVideoCloseTo1M = query(async () => {
"use server";
return dbMain
.select()
.from(bilibiliMetadata)
.innerJoin(latestVideoSnapshot, eq(latestVideoSnapshot.aid, bilibiliMetadata.aid))
.where(and(gte(latestVideoSnapshot.views, 900000), lt(latestVideoSnapshot.views, 1000000)))
.orderBy(desc(latestVideoSnapshot.views))
.limit(20);
}, "videosCloseTo1M");
interface VideoCardProps {
video: {
bilibili_metadata: BilibiliMetadataType;
latest_video_snapshot: LatestVideoSnapshotType;
};
}
export const route = {
preload: () => getVideoCloseTo1M()
} satisfies RouteDefinition;
const VideoCard: Component<VideoCardProps> = (props) => {
return (
<Card variant="outlined" class="w-64 h-64 grow-0 shrink-0 basis-64">
<CardMedia
class="w-64 h-32 object-cover"
round={false}
src={props.video.bilibili_metadata.coverUrl || ""}
referrerpolicy="no-referrer"
/>
<CardContent class="py-3 px-4">
<Typography.Body variant="large" class="text-wrap">
{props.video.bilibili_metadata.title}
</Typography.Body>
<span>{props.video.latest_video_snapshot.views} </span>
</CardContent>
</Card>
);
};
export default function Home() {
const videos = createAsync(() => getVideoCloseTo1M());
return (
<Layout>
<title>V档案馆</title>
<main class="w-full pt-20 lg:max-w-3xl xl:max-w-4xl 2xl:max-w-6xl lg:mx-auto">
<h1 class="text-4xl mb-8"> V </h1>
<h2 class="text-2xl font-normal"></h2>
<div
class="flex overflow-x-auto overflow-y-hidden gap-4 whitespace-nowrap w-full
py-2 px-4 mt-2 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
>
<Suspense fallback={<div>Loading...</div>}>
<For each={videos()}>{(video) => <VideoCard video={video} />}</For>
</Suspense>
</div>
</main>
</Layout>
);
}

View File

@ -0,0 +1,208 @@
import { DateTime } from "luxon";
import { useParams } from "@solidjs/router";
import { createResource } from "solid-js";
import { Suspense } from "solid-js";
import { For } from "solid-js";
import { useCachedFetch } from "~/lib/dbCache";
import { dbMain } from "~/drizzle";
import { bilibiliMetadata, videoSnapshot } from "~db/main/schema";
import { desc, eq } from "drizzle-orm";
import { BilibiliMetadataType, VideoSnapshotType } from "~db/outerSchema";
import { Context, useRequestContext } from "~/components/requestContext";
import { Layout } from "~/components/layout";
async function getAllSnapshots(aid: number, context: Context) {
"use server";
return useCachedFetch(
async () => {
return dbMain
.select()
.from(videoSnapshot)
.where(eq(videoSnapshot.aid, aid))
.orderBy(desc(videoSnapshot.createdAt));
},
"all-snapshots",
context,
[aid]
);
}
async function getVideoMetadata(avORbv: number | string, context: Context) {
"use server";
if (typeof avORbv === "number") {
return useCachedFetch(
async () => {
return dbMain.select().from(bilibiliMetadata).where(eq(bilibiliMetadata.aid, avORbv)).limit(1);
},
"bili-metadata",
context,
[avORbv]
);
} else {
return useCachedFetch(
async () => {
return dbMain.select().from(bilibiliMetadata).where(eq(bilibiliMetadata.bvid, avORbv)).limit(1);
},
"bili-metadata",
context,
[avORbv]
);
}
}
const MetadataRow = ({ title, desc }: { title: string; desc: string | number | undefined | null }) => {
if (!desc) return <></>;
return (
<tr>
<td class="max-w-14 min-w-14 md:max-w-24 md:min-w-24 border dark:border-zinc-500 px-2 md:px-3 py-2 font-semibold">
{title}
</td>
<td class="break-all max-w-[calc(100vw-4.5rem)] border dark:border-zinc-500 px-4 py-2">{desc}</td>
</tr>
);
};
export default function VideoInfoPage() {
const params = useParams();
const { id } = params;
const context = useRequestContext();
const [data] = createResource(async () => {
let videoInfo: BilibiliMetadataType | null = null;
let snapshots: VideoSnapshotType[] = [];
try {
const videoData = await getVideoMetadata(id, context);
if (videoData.length === 0) {
return null;
}
const snapshotsData = await getAllSnapshots(videoData[0].aid, context);
videoInfo = videoData[0];
if (snapshotsData) {
snapshots = snapshotsData;
}
} catch (e) {
console.error(e);
}
if (!videoInfo) {
return null;
}
const title = `${videoInfo.title} - 歌曲信息 - 中 V 档案馆`;
return {
v: videoInfo,
s: snapshots,
t: title
};
});
return (
<Layout>
<main class="flex flex-col items-center min-h-screen gap-8 mt-10 md:mt-6 relative z-0 overflow-x-auto pb-8">
<div class="w-full lg:max-w-4xl lg:mx-auto lg:p-6">
<Suspense fallback={<div>loading</div>}>
<title>{data()?.t}</title>
<span>{data()?.t}</span>
<h1 class="text-2xl font-medium ml-2 mb-4">
:{" "}
<a href={`https://www.bilibili.com/video/av${data()?.v.aid}`} class="underline">
av{data()?.v.aid}
</a>
</h1>
<div class="mb-6">
<h2 class="px-2 mb-2 text-xl font-medium"></h2>
<div class="overflow-x-auto max-w-full px-2">
<table class="table-fixed">
<tbody>
<MetadataRow title="ID" desc={data()?.v.id} />
<MetadataRow title="av 号" desc={data()?.v.aid} />
<MetadataRow title="BV 号" desc={data()?.v.bvid} />
<MetadataRow title="标题" desc={data()?.v.title} />
<MetadataRow title="描述" desc={data()?.v.description} />
<MetadataRow title="UID" desc={data()?.v.uid} />
<MetadataRow title="标签" desc={data()?.v.tags} />
<MetadataRow
title="发布时间"
desc={
data()?.v.publishedAt
? DateTime.fromJSDate(
new Date(data()?.v.publishedAt || "")
).toFormat("yyyy-MM-dd HH:mm:ss")
: null
}
/>
<MetadataRow title="时长 (秒)" desc={data()?.v.duration} />
<MetadataRow
title="创建时间"
desc={DateTime.fromJSDate(new Date(data()?.v.createdAt || "")).toFormat(
"yyyy-MM-dd HH:mm:ss"
)}
/>
<MetadataRow title="封面" desc={data()?.v?.coverUrl} />
</tbody>
</table>
</div>
</div>
<div>
<h2 class="px-2 mb-2 text-xl font-medium"></h2>
<div class="overflow-x-auto px-2">
<table class="table-auto w-full">
<thead>
<tr>
<th class="border dark:border-zinc-500 px-4 py-2 font-medium"></th>
<th class="border dark:border-zinc-500 px-4 py-2 font-medium"></th>
<th class="border dark:border-zinc-500 px-4 py-2 font-medium"></th>
<th class="border dark:border-zinc-500 px-4 py-2 font-medium"></th>
<th class="border dark:border-zinc-500 px-4 py-2 font-medium"></th>
<th class="border dark:border-zinc-500 px-4 py-2 font-medium"></th>
<th class="border dark:border-zinc-500 px-4 py-2 font-medium"></th>
<th class="border dark:border-zinc-500 px-4 py-2 font-medium"></th>
</tr>
</thead>
<tbody>
<For each={data()?.s}>
{(snapshot) => (
<tr>
<td class="border dark:border-zinc-500 px-4 py-2">
{DateTime.fromJSDate(new Date(snapshot.createdAt)).toFormat(
"yyyy-MM-dd HH:mm:ss"
)}
</td>
<td class="border dark:border-zinc-500 px-4 py-2">
{snapshot.views}
</td>
<td class="border dark:border-zinc-500 px-4 py-2">
{snapshot.coins}
</td>
<td class="border dark:border-zinc-500 px-4 py-2">
{snapshot.likes}
</td>
<td class="border dark:border-zinc-500 px-4 py-2">
{snapshot.favorites}
</td>
<td class="border dark:border-zinc-500 px-4 py-2">
{snapshot.shares}
</td>
<td class="border dark:border-zinc-500 px-4 py-2">
{snapshot.danmakus}
</td>
<td class="border dark:border-zinc-500 px-4 py-2">
{snapshot.replies}
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
</Suspense>
</div>
</main>
</Layout>
);
}

View File

@ -0,0 +1,80 @@
import { Layout } from "~/components/layout";
import { LeftSideBar } from "~/components/song/LeftSideBar";
import { RightSideBar } from "~/components/song/RightSideBar";
import { Content } from "~/components/song/Content";
import { createAsync, query, RouteDefinition, useParams } from "@solidjs/router";
import { dbMain } from "~/drizzle";
import { bilibiliMetadata, songs } from "~db/main/schema";
import { eq } from "drizzle-orm";
const getVideoAID = async (id: string) => {
"use server";
if (id.startsWith("av")) {
return parseInt(id.slice(2));
} else if (id.startsWith("BV")) {
const data = await dbMain
.select()
.from(bilibiliMetadata)
.where(eq(bilibiliMetadata.bvid, id));
return data[0].aid;
}
else {
return null;
}
};
const findSongIDFromAID = async (aid: number) => {
"use server";
const data = await dbMain.select({
id: songs.id,
}).from(songs).where(eq(songs.aid, aid)).limit(1);
return data[0].id;
}
const getSongInfo = query(async (songID: number) => {
"use server";
const data = await dbMain.select().from(songs).where(eq(songs.id, songID));
return data[0] || null;
}, "songs");
const getSongInfoFromID = query(async (id: string) => {
"use server";
const aid = await getVideoAID(id);
if (!aid && parseInt(id)) {
return getSongInfo(parseInt(id));
}
else if (!aid) {
return null;
}
const songID = await findSongIDFromAID(aid);
return getSongInfo(songID);
}, "songsRaw")
export const route = {
preload: ({ params }) => getSongInfoFromID(params.id)
} satisfies RouteDefinition;
export default function Info() {
const params = useParams();
const info = createAsync(() => getSongInfoFromID(params.id));
return (
<Layout>
<title> - - V </title>
<div
class="pt-8 w-full sm:w-120 sm:mx-auto lg:w-full 2xl:w-360 lg:grid lg:grid-cols-[1fr_560px_1fr]
xl:grid-cols-[1fr_648px_1fr]"
>
<nav class="top-32 hidden lg:block pb-12 px-6 self-start sticky">
<LeftSideBar />
</nav>
<main class="mb-24">
<Content data={info() || null}/>
</main>
<div class="top-32 hidden lg:flex self-start sticky flex-col pb-12 px-6">
<RightSideBar />
</div>
</div>
</Layout>
);
}

View File

@ -0,0 +1,9 @@
import { Layout } from "~/components/layout";
export default function SongsHome() {
return (
<Layout>
<h1 class="text-4xl mb-8"></h1>
</Layout>
);
}

View File

@ -0,0 +1,56 @@
function generateSpacing(nums: number[], divisor = 4): Record<number, string> {
return nums.reduce<Record<number, string>>((acc, n) => {
acc[n] = `${n / divisor}rem`;
return acc;
}, {});
}
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {
spacing: generateSpacing([360]),
colors: {
background: "var(--md-sys-color-background)",
"on-background": "var(--md-sys-color-on-background)",
surface: "var(--md-sys-color-surface)",
"surface-dim": "var(--md-sys-color-surface-dim)",
"surface-bright": "var(--md-sys-color-surface-bright)",
"surface-container-lowest": "var(--md-sys-color-surface-container-lowest)",
"surface-container-low": "var(--md-sys-color-surface-container-low)",
"surface-container": "var(--md-sys-color-surface-container)",
"surface-container-high": "var(--md-sys-color-surface-container-high)",
"surface-container-highest": "var(--md-sys-color-surface-container-highest)",
"on-surface": "var(--md-sys-color-on-surface)",
"surface-variant": "var(--md-sys-color-surface-variant)",
"on-surface-variant": "var(--md-sys-color-on-surface-variant)",
"inverse-surface": "var(--md-sys-color-inverse-surface)",
"inverse-on-surface": "var(--md-sys-color-inverse-on-surface)",
outline: "var(--md-sys-color-outline)",
"outline-variant": "var(--md-sys-color-outline-variant)",
shadow: "var(--md-sys-color-shadow)",
scrim: "var(--md-sys-color-scrim)",
"surface-tint": "var(--md-sys-color-surface-tint)",
primary: "var(--md-sys-color-primary)",
"on-primary": "var(--md-sys-color-on-primary)",
"primary-container": "var(--md-sys-color-primary-container)",
"on-primary-container": "var(--md-sys-color-on-primary-container)",
"inverse-primary": "var(--md-sys-color-inverse-primary)",
secondary: "var(--md-sys-color-secondary)",
"on-secondary": "var(--md-sys-color-on-secondary)",
"secondary-container": "var(--md-sys-color-secondary-container)",
"on-secondary-container": "var(--md-sys-color-on-secondary-container)",
tertiary: "var(--md-sys-color-tertiary)",
"on-tertiary": "var(--md-sys-color-on-tertiary)",
"tertiary-container": "var(--md-sys-color-tertiary-container)",
"on-tertiary-container": "var(--md-sys-color-on-tertiary-container)",
error: "var(--md-sys-color-error)",
"on-error": "var(--md-sys-color-on-error)",
"error-container": "var(--md-sys-color-error-container)",
"on-error-container": "var(--md-sys-color-on-error-container)"
}
}
},
plugins: []
};

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"allowJs": true,
"strict": true,
"noEmit": true,
"types": ["vinxi/types/client"],
"isolatedModules": true,
"paths": {
"~/*": ["./src/*"],
"~db/*": ["./src/drizzle/*"]
}
}
}

View File

@ -1,6 +1,3 @@
.DS_Store
/node_modules/
# React Router
/.react-router/
/build/
.react-router/
build/

View File

@ -1,9 +0,0 @@
// Generated by React Router
import "react-router";
declare module "react-router" {
interface Future {
v8_middleware: false
}
}

View File

@ -1,36 +0,0 @@
// Generated by React Router
import "react-router"
declare module "react-router" {
interface Register {
pages: Pages
routeFiles: RouteFiles
}
}
type Pages = {
"/": {
params: {};
};
"/song/:id/info": {
params: {
"id": string;
};
};
};
type RouteFiles = {
"root.tsx": {
id: "root";
page: "/" | "/song/:id/info";
};
"routes/home.tsx": {
id: "routes/home";
page: "/";
};
"routes/song/[id]/info.tsx": {
id: "routes/song/[id]/info";
page: "/song/:id/info";
};
};

View File

@ -1,17 +0,0 @@
// Generated by React Router
declare module "virtual:react-router/server-build" {
import { ServerBuild } from "react-router";
export const assets: ServerBuild["assets"];
export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"];
export const basename: ServerBuild["basename"];
export const entry: ServerBuild["entry"];
export const future: ServerBuild["future"];
export const isSpaMode: ServerBuild["isSpaMode"];
export const prerender: ServerBuild["prerender"];
export const publicPath: ServerBuild["publicPath"];
export const routeDiscovery: ServerBuild["routeDiscovery"];
export const routes: ServerBuild["routes"];
export const ssr: ServerBuild["ssr"];
export const unstable_getCriticalCss: ServerBuild["unstable_getCriticalCss"];
}

View File

@ -1,59 +0,0 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../root.js")
type Info = GetInfo<{
file: "root.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../root.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
}

View File

@ -1,62 +0,0 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../home.js")
type Info = GetInfo<{
file: "routes/home.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/home";
module: typeof import("../home.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
}

View File

@ -1,62 +0,0 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../info.js")
type Info = GetInfo<{
file: "routes/song/[id]/info.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../../../root.js");
}, {
id: "routes/song/[id]/info";
module: typeof import("../info.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
}

118
src/fillSongInfo.ts Normal file
View File

@ -0,0 +1,118 @@
import arg from "arg";
import logger from "@core/log/logger";
import { sql } from "@core/index";
import type { Row } from "postgres";
const quit = (reason?: string) => {
reason && logger.error(reason);
process.exit();
};
const args = arg({
"--file": String
});
const dataPath = args["--file"];
if (!dataPath) {
quit("Missing --file <path>");
}
interface Item {
name: string;
singer: string[];
}
type DataFile = {
[key: string]: Item;
};
const pg = sql;
async function getVideoInfo(id: string): Promise<Row | undefined> {
if (parseInt(id)) {
return (
await pg`
SELECT aid, bvid
FROM bilibili_metadata
WHERE aid = ${id}
`
)[0];
} else if (id.startsWith("av")) {
return (
await pg`
SELECT aid, bvid
FROM bilibili_metadata
WHERE aid = ${id.replace("av", "")}
`
)[0];
} else if (id.startsWith("BV")) {
return (
await pg`
SELECT aid, bvid
FROM bilibili_metadata
WHERE bvid = ${id}
`
)[0];
} else {
return undefined;
}
}
async function getSingerID(name: string): Promise<number | undefined> {
const singer = await pg`
SELECT id
FROM singer
WHERE name = ${name}
`;
if (singer.length > 0) {
return singer[0]?.id;
}
const singerID = await pg`
INSERT INTO singer (name)
VALUES (${name})
RETURNING id
`;
return singerID[0]?.id;
}
async function processVideo(key: string, item: Item) {
const videoInfo = await getVideoInfo(key);
if (!videoInfo) {
logger.warn(`Video not found: ${key}`);
return;
}
const aid = videoInfo.aid;
await pg`
UPDATE songs
SET name = ${item.name}
WHERE aid = ${aid}
`;
const singerIDs = (await Promise.all(item.singer.map(async (singer) => await getSingerID(singer)))).filter(
(id) => id !== undefined
);
for (const singerID of singerIDs) {
await pg`
INSERT INTO
relations (source_id, source_type, target_id, target_type, relation)
VALUES (${aid}, 'song', ${singerID}, 'singer', 'sing')
ON CONFLICT (source_id, source_type, target_id, target_type, relation) DO NOTHING;
`;
}
}
async function fillSongInfo() {
let fixQuery = "";
let i = 0;
const file = Bun.file(dataPath!);
const candidates: DataFile = await file.json();
const length = Object.keys(candidates).length;
for (const videoID in candidates) {
await processVideo(videoID, candidates[videoID]!);
i++;
logger.log(`Progress: ${i}/${length}`);
}
return fixQuery;
}
await fillSongInfo();
quit();

68
src/fixCover.ts Normal file
View File

@ -0,0 +1,68 @@
import arg from "arg";
import { Database } from "bun:sqlite";
import logger from "@core/log/logger";
import type { VideoDetailsData } from "@core/net/bilibili.d.ts";
import { sql } from "@core/index";
const quit = (reason?: string) => {
reason && logger.error(reason);
process.exit();
};
const args = arg({
"--db": String
});
const dbPath = args["--db"];
if (!dbPath) {
quit("Missing --db <path>");
}
const sqlite = new Database(dbPath);
const pg = sql;
async function fixMissingCover() {
let fixQuery = "";
let i = 0;
let j = 0;
const candidates = await pg`
SELECT aid
FROM
bilibili_metadata
WHERE
cover_url IS NULL
`;
const query = sqlite.query(`SELECT data FROM bili_info_crawl WHERE aid = $aid`);
for (const video of candidates) {
j++;
logger.log(`Progress: ${j}/${candidates.length}`);
const aid: number = video.aid;
try {
const sqliteData: any = query.get({ $aid: aid });
const rawData: VideoDetailsData | null = JSON.parse(sqliteData.data);
if (!rawData) {
logger.warn(`Data not exists for aid: ${aid}`);
continue;
}
const coverURL = rawData.View.pic;
if (!coverURL) continue;
const q = `UPDATE bilibili_metadata SET cover_url = '${coverURL}' WHERE aid = ${aid};\n`;
logger.log(`Fixing cover for aid: ${aid}`);
i++;
fixQuery += q;
} catch (e) {
//logger.error(e as Error, undefined, aid.toString());
logger.error(aid.toString());
}
if (j % 1000 === 0) {
const bytes = await Bun.write("scripts/fix_2.sql", fixQuery);
logger.warn(`Wrote ${bytes} bytes`, "backup");
}
}
logger.log(`Fixed ${i} videos, query length ${fixQuery.length}.`);
return fixQuery;
}
const q = await fixMissingCover();
await Bun.write("scripts/fix_2.sql", q);
quit();

70
src/fixPubDate.ts Normal file
View File

@ -0,0 +1,70 @@
import arg from "arg";
import { Database } from "bun:sqlite";
import logger from "@core/log/logger";
import type { VideoDetailsData } from "@core/net/bilibili.d.ts";
import { sql } from "@core/index";
const quit = (reason?: string) => {
reason && logger.error(reason);
process.exit();
};
const args = arg({
"--db": String
});
const dbPath = args["--db"];
if (!dbPath) {
quit("Missing --db <path>");
}
const sqlite = new Database(dbPath);
const pg = sql;
async function fixTimezoneError() {
let fixQuery = "";
let i = 0;
let j = 0;
const candidates = await pg`
SELECT aid, published_at
FROM
bilibili_metadata
WHERE
published_at >= '2025-04-26'
AND published_at <= '2025-06-01'
AND status = 0
`;
const query = sqlite.query(`SELECT data FROM bili_info_crawl WHERE aid = $aid`);
for (const video of candidates) {
const aid: number = video.aid;
try {
const sqliteData: any = query.get({ $aid: aid });
const rawData: VideoDetailsData | null = JSON.parse(sqliteData.data);
if (!rawData) {
logger.warn(`Data not exists for aid: ${aid}`);
continue;
}
const realTimestamp = rawData.View.pubdate;
const dbTimestamp = video.published_at.getTime() / 1000;
const diff = dbTimestamp - realTimestamp;
if (Math.abs(diff) > 1) {
logger.warn(`Find incorrect timestamp for aid ${aid} with diff of ${diff} sec`);
const date = new Date(realTimestamp * 1000).toISOString();
const q = `UPDATE bilibili_metadata SET published_at = '${date}' WHERE aid = ${aid};\n`;
fixQuery += q;
i++;
}
} catch (e) {
//logger.error(e as Error, undefined, aid.toString());
logger.error(aid.toString());
}
j++;
logger.log(`Progress: ${j}/${candidates.length}`);
}
logger.log(`Fixed ${i} videos, query length ${fixQuery.length}.`);
return fixQuery;
}
const q = await fixTimezoneError();
await Bun.write("scripts/fix.sql", q);
quit();

View File

@ -1,32 +0,0 @@
import arg from "arg";
//import { getVideoDetails } from "@crawler/net/getVideoDetails";
import logger from "@core/log/logger";
const quit = (reason: string) => {
logger.error(reason);
process.exit();
};
const args = arg({
"--aids": String // --port <number> or --port=<number>
});
const aidsFileName = args["--aids"];
if (!aidsFileName) {
quit("Missing --aids <file_path>");
}
const aidsFile = Bun.file(aidsFileName!);
const fileExists = await aidsFile.exists();
if (!fileExists) {
quit(`${aidsFile} does not exist.`);
}
const aidsText = await aidsFile.text();
const aids = aidsText
.split("\n")
.map((line) => parseInt(line))
.filter((num) => !Number.isNaN(num));
logger.log(`Read ${aids.length} aids.`);