Compare commits

...

14 Commits

38 changed files with 792 additions and 321 deletions

View File

@ -2,4 +2,5 @@ data
*.json
*.svg
*.txt
*.md
*.md
*config*

View File

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

View File

@ -8,6 +8,7 @@ if (unsetVars.length > 0) {
const databaseHost = Deno.env.get("DB_HOST")!;
const databaseName = Deno.env.get("DB_NAME")!;
const databaseNameCred = Deno.env.get("DB_NAME_CRED")!;
const databaseUser = Deno.env.get("DB_USER")!;
const databasePassword = Deno.env.get("DB_PASSWORD")!;
const databasePort = Deno.env.get("DB_PORT")!;
@ -19,3 +20,11 @@ export const postgresConfig = {
user: databaseUser,
password: databasePassword,
};
export const postgresConfigCred = {
hostname: databaseHost,
port: parseInt(databasePort),
database: databaseNameCred,
user: databaseUser,
password: databasePassword,
}

View File

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

View File

@ -371,8 +371,8 @@ export const scheduleCleanupWorker = async (_job: Job) => {
const query = `
SELECT id, aid, type
FROM snapshot_schedule
WHERE status IN ('pending', 'processing')
AND started_at < NOW() - INTERVAL '5 minutes'
WHERE status IN ('pending', 'processing')
AND started_at < NOW() - INTERVAL '30 minutes'
`;
const { rows } = await client.queryObject<{ id: bigint; aid: bigint; type: string }>(query);
if (rows.length === 0) return;

View File

@ -1,50 +0,0 @@
# Astro Starter Kit: Basics
```sh
deno create astro@latest -- --template basics
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
│ └── favicon.svg
├── src/
│ ├── layouts/
│ │ └── Layout.astro
│ └── pages/
│ └── index.astro
└── package.json
```
To learn more about the folder structure of an Astro project, refer to
[our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :--------------------- | :----------------------------------------------- |
| `deno install` | Installs dependencies |
| `deno dev` | Starts local dev server at `localhost:4321` |
| `deno build` | Build your production site to `./dist/` |
| `deno preview` | Preview your build locally, before deploying |
| `deno astro ...` | Run CLI commands like `astro add`, `astro check` |
| `deno astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our
[Discord server](https://astro.build/chat).

View File

@ -1,13 +1,24 @@
// @ts-check
import { defineConfig } from "astro/config";
import tailwind from "@astrojs/tailwind";
// https://astro.build/config
import tsconfigPaths from "vite-tsconfig-paths";
import node from "@astrojs/node";
import svelte from "@astrojs/svelte";
export default defineConfig({
output: "server",
adapter: node({
mode: "standalone",
}),
integrations: [tailwind(), svelte()],
vite: {
server: {
fs: {
allow: [".", "../../"],
},
},
plugins: [tsconfigPaths()],
},
});

View File

@ -0,0 +1,8 @@
{
"name": "@cvsa/frontend",
"tasks": {
"preview": "bun run astro preview",
"build": "bun run astro build"
},
"exports": "./main.ts"
}

View File

@ -0,0 +1 @@
export const VERSION = "1.2.6";

View File

@ -1,14 +1,23 @@
{
"name": "frontend",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"astro": "^5.5.5"
}
"name": "frontend",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/tailwind": "^6.0.2",
"astro": "^5.5.5",
"autoprefixer": "^10.4.21",
"pg": "^8.11.11",
"postcss": "^8.5.3",
"tailwindcss": "^3.0.24",
"vite-tsconfig-paths": "^5.1.4"
},
"devDependencies": {
"@types/pg": "^8.11.11"
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 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: 22 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 1.4 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: 22 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: 22 KiB

View File

@ -0,0 +1,12 @@
<script lang="ts">
export let className = "";
</script>
<div class={className}>
<svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6z"
/>
</svg>
</div>

View File

@ -0,0 +1,41 @@
<script lang="ts">
import { onMount } from "svelte";
import { fade } from "svelte/transition";
export let lightSrc: string;
export let darkSrc: string;
export let alt: string = "";
export let className: string = "";
let isDarkMode = false;
let currentSrc: string;
let opacity = 0;
onMount(() => {
const handleDarkModeChange = (event: MediaQueryListEvent) => {
isDarkMode = event.matches;
currentSrc = isDarkMode ? darkSrc : lightSrc;
opacity = 1;
};
const darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
isDarkMode = darkModeMediaQuery.matches;
currentSrc = isDarkMode ? darkSrc : lightSrc;
opacity = 1;
darkModeMediaQuery.addEventListener("change", handleDarkModeChange);
return () => {
darkModeMediaQuery.removeEventListener("change", handleDarkModeChange);
};
});
$: currentSrc = isDarkMode ? darkSrc : lightSrc;
</script>
<img
src={currentSrc}
{alt}
class={className}
style={`opacity: ${opacity}`}
/>

View File

@ -0,0 +1,20 @@
<script lang="ts">
export let className = '';
</script>
<div class={className}>
<svg width="28.000000" height="28.000000" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="clip97_210">
<rect id="菜单按钮" width="28.000000" height="28.000000" fill="white" fill-opacity="0"/>
</clipPath>
</defs>
<g clip-path="url(#clip97_210)">
<path id="path" d="M4.66 21C4.33 21 4.05 20.88 3.83 20.66C3.61 20.44 3.5 20.16 3.5 19.83C3.49 19.5 3.61 19.22 3.83 19C4.06 18.77 4.33 18.66 4.66 18.66L23.33 18.66C23.66 18.66 23.94 18.77 24.16 19C24.38 19.22 24.5 19.5 24.5 19.83C24.49 20.16 24.38 20.44 24.16 20.66C23.94 20.88 23.66 21 23.33 21L4.66 21ZM4.66 15.16C4.33 15.16 4.05 15.05 3.83 14.83C3.61 14.6 3.5 14.32 3.5 14C3.49 13.67 3.61 13.39 3.83 13.16C4.06 12.94 4.33 12.83 4.66 12.83L23.33 12.83C23.66 12.83 23.94 12.94 24.16 13.16C24.38 13.39 24.5 13.67 24.5 14C24.49 14.32 24.38 14.6 24.16 14.83C23.94 15.05 23.66 15.16 23.33 15.16L4.66 15.16ZM4.66 9.33C4.33 9.33 4.05 9.22 3.83 8.99C3.61 8.77 3.5 8.49 3.5 8.16C3.49 7.83 3.61 7.56 3.83 7.33C4.06 7.11 4.33 7 4.66 7L23.33 7C23.66 7 23.94 7.11 24.16 7.33C24.38 7.56 24.5 7.83 24.5 8.16C24.49 8.49 24.38 8.77 24.16 8.99C23.94 9.22 23.66 9.33 23.33 9.33L4.66 9.33Z" fill="currentColor" fill-opacity="1.000000" fill-rule="nonzero"/>
</g>
</svg>
</div>

View File

@ -0,0 +1,44 @@
<script lang="ts">
let inputBox: HTMLInputElement | null = null;
export function changeFocusState(target: boolean) {
if (!inputBox) return;
if (target) {
inputBox.focus();
} else {
inputBox.blur();
}
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === "Enter") {
event.preventDefault();
const input = event.target as HTMLInputElement;
const value: string = input.value.trim();
if (!value) return;
window.location.href = `/song/${value}/info`;
}
}
</script>
<!-- svelte-ignore a11y_autofocus -->
<div
class="absolute left-0 md:left-96 ml-4 w-[calc(100%-5rem)] md:w-[calc(100%-40rem)] 2xl:max-w-[50rem] 2xl:left-1/2 2xl:-translate-x-1/2 inline-flex items-center h-full"
>
<input
bind:this={inputBox}
type="search"
placeholder="搜索"
class="top-0 w-full h-10 px-4 rounded-lg bg-white/80 dark:bg-zinc-800/70
backdrop-blur-lg border border-zinc-300 dark:border-zinc-600 focus:border-zinc-400 duration-200 transition-colors focus:outline-none"
on:keydown={handleKeydown}
/>
</div>
<style>
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
appearance: none;
}
</style>

View File

@ -0,0 +1,12 @@
<script lang="ts">
export let className = '';
</script>
<div class={className}>
<svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24">
<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"
></path>
</svg>
</div>

View File

@ -0,0 +1,30 @@
---
import astroLogoLight from "@assets/标题-浅色.svg";
import astroLogoDark from "@assets/标题-深色.svg";
import DarkModeImage from "@components/DarkModeImage.svelte";
import SearchBox from "@components/SearchBox.svelte";
import TitleBarMobile from "@components/TitleBarMobile.svelte";
---
<div class="hidden md:block fixed top-0 left-0 w-full h-28 bg-white/80 dark:bg-zinc-900/70 backdrop-blur-lg z-50">
<div class="w-[305px] ml-8 inline-flex h-full items-center">
<a href="/">
<DarkModeImage
lightSrc={astroLogoLight.src}
darkSrc={astroLogoDark.src}
alt="Logo"
className="w-[305px] h-24 inline-block"
client:load
/>
</a>
</div>
<SearchBox client:load />
<div
class="inline-flex right-12 absolute gap-4 h-full text-xl dark:text-[#C6DCF2] font-medium items-center w-48 justify-end"
>
<a href="/about" class="hover:dark:text-[#B1C5DA]">关于</a>
</div>
</div>
<TitleBarMobile client:load />

View File

@ -0,0 +1,47 @@
<script lang="ts">
import logoMobileDark from "@assets/TitleBar Mobile Dark.svg";
import logoMobileLight from "@assets/TitleBar Mobile Light.svg";
import SearchBox from "./SearchBox.svelte";
import SearchIcon from "./SearchIcon.svelte";
import MenuIcon from "./MenuIcon.svelte";
import DarkModeImage from "./DarkModeImage.svelte";
import CloseIcon from "./CloseIcon.svelte";
let searchBox: SearchBox | null = null;
let showSearchBox = false;
$: if (showSearchBox && searchBox) {
searchBox.changeFocusState(true);
}
</script>
<div class="md:hidden fixed top-0 left-0 w-full h-16 bg-white/80 dark:bg-zinc-800/70 backdrop-blur-lg z-50">
{#if !showSearchBox}
<button class="inline-block ml-4 mt-4 dark:text-white">
<MenuIcon />
</button>
<div class="ml-8 inline-flex h-full items-center">
<a href="/">
<DarkModeImage
lightSrc={logoMobileLight.src}
darkSrc={logoMobileDark.src}
alt="Logo"
className="w-24 h-8 translate-y-[2px]"
/>
</a>
</div>
{/if}
{#if showSearchBox}
<SearchBox bind:this={searchBox} />
{/if}
<button
class="inline-flex absolute right-0 h-full items-center mr-4"
onclick={() => (showSearchBox = !showSearchBox)}
>
{#if showSearchBox}
<CloseIcon />
{:else}
<SearchIcon />
{/if}
</button>
</div>

View File

@ -1,210 +1,10 @@
---
import astroLogo from '../assets/astro.svg';
import background from '../assets/background.svg';
import TitleBar from "@components/TitleBar.astro";
---
<div id="container">
<img id="background" src={background.src} alt="" fetchpriority="high" />
<main>
<section id="hero">
<a href="https://astro.build"
><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
>
<h1>
To get started, open the <code><pre>src/pages</pre></code> directory in your project.
</h1>
<section id="links">
<a class="button" href="https://docs.astro.build">Read our docs</a>
<a href="https://astro.build/chat"
>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
><path
fill="currentColor"
d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
></path></svg
>
</a>
</section>
</section>
</main>
<TitleBar/>
<a href="https://astro.build/blog/astro-5/" id="news" class="box">
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
><path
d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
fill="#111827"></path></svg
>
<h2>What's New in Astro 5.0?</h2>
<p>
From content layers to server islands, click to learn more about the new features and
improvements in Astro 5.0
</p>
</a>
</div>
<style>
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
filter: blur(100px);
}
#container {
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
height: 100%;
}
main {
height: 100%;
display: flex;
justify-content: center;
}
#hero {
display: flex;
align-items: start;
flex-direction: column;
justify-content: center;
padding: 16px;
}
h1 {
font-size: 22px;
margin-top: 0.25em;
}
#links {
display: flex;
gap: 16px;
}
#links a {
display: flex;
align-items: center;
padding: 10px 12px;
color: #111827;
text-decoration: none;
transition: color 0.2s;
}
#links a:hover {
color: rgb(78, 80, 86);
}
#links a svg {
height: 1em;
margin-left: 8px;
}
#links a.button {
color: white;
background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
inset 0 -2px 0 rgba(0, 0, 0, 0.24);
border-radius: 10px;
}
#links a.button:hover {
color: rgb(230, 230, 230);
box-shadow: none;
}
pre {
font-family:
ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
monospace;
font-weight: normal;
background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
}
h2 {
margin: 0 0 1em;
font-weight: normal;
color: #111827;
font-size: 20px;
}
p {
color: #4b5563;
font-size: 16px;
line-height: 24px;
letter-spacing: -0.006em;
margin: 0;
}
code {
display: inline-block;
background:
linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
border-radius: 8px;
border: 1px solid transparent;
padding: 6px 8px;
}
.box {
padding: 16px;
background: rgba(255, 255, 255, 1);
border-radius: 16px;
border: 1px solid white;
}
#news {
position: absolute;
bottom: 16px;
right: 16px;
max-width: 300px;
text-decoration: none;
transition: background 0.2s;
backdrop-filter: blur(50px);
}
#news:hover {
background: rgba(255, 255, 255, 0.55);
}
@media screen and (max-height: 368px) {
#news {
display: none;
}
}
@media screen and (max-width: 768px) {
#container {
display: flex;
flex-direction: column;
}
#hero {
display: block;
padding-top: 10%;
}
#links {
flex-wrap: wrap;
}
#links a.button {
padding: 14px 18px;
}
#news {
right: 16px;
left: 16px;
bottom: 2.5rem;
max-width: 100%;
}
h1 {
line-height: 1.5;
}
}
</style>
<main class="flex flex-col items-center justify-center min-h-screen gap-8">
<h1 class="text-4xl font-bold text-center">正在施工中……</h1>
<p>在搜索栏输入BV号或AV号可以查询目前数据库收集到的信息~</p>
</main>

View File

@ -0,0 +1,60 @@
# 关于「中V档案馆」
「中V档案馆」是一个旨在收录与展示「中文歌声合成作品」及有关信息的网站。
## 创建背景与关联工作
纵观整个互联网对于「中文歌声合成」或「中文虚拟歌手」常简称为中V或VC相关信息进行较为系统、全面地整理收集的主要有以下几个网站
- [萌娘百科](https://zh.moegirl.org.cn/):
收录了大量中V歌曲及歌姬的信息呈现形式为传统维基基于[MediaWiki](https://www.mediawiki.org/))。
- [VCPedia](https://vcpedia.cn/):
由原萌娘百科中文歌声合成编辑团队的部分成员搭建,专属于中文歌声合成相关内容的信息集成站点[^1],呈现形式为传统维基(基于[MediaWiki](https://www.mediawiki.org/))。
- [VocaDB](https://vocadb.net/): 一个围绕 Vocaloid、UTAU 和其他歌声合成器的协作数据库其中包含艺术家、唱片、PV
等[^2],其中包含大量中文歌声合成作品。
- [天钿Daily](https://tdd.bunnyxt.com/)一个VC相关数据交流与分享的网站。致力于VC相关数据交流定期抓取VC相关数据选取有意义的纬度展示。[^3]
上述网站中,或多或少存在一些不足,例如:
- 萌娘百科、VCPedia受限于传统维基绝大多数内容依赖人工编辑。
- VocaDB基于结构化数据库构建由此可以依赖程序生成一些信息但**条目收录**仍然完全依赖人工完成。
- VocaDB主要专注于元数据展示少有关于歌曲、作者等的描述性的文字也缺乏描述性的背景信息。
- 天钿Daily只展示歌曲的统计数据及历史趋势没有关于歌曲其它信息的收集。
因此,**中V档案馆**吸取前人经验,克服上述网站的不足,希望做到:
- 歌曲收录(指发现歌曲并创建条目)的完全自动化
- 歌曲元信息提取的高度自动化
- 歌曲统计数据收集的完全自动化
- 在程序辅助的同时欢迎并鼓励贡献者参与编辑(主要为描述性内容)或纠错
- 在适当的许可声明下,引用来自上述源的数据,使内容更加全面、丰富。
## 技术架构
参见[CVSA文档](https://cvsa.gitbook.io/)。
## 开放许可
受本文以[CC BY-NC-SA 4.0协议](https://creativecommons.org/licenses/by-nc-sa/4.0/)提供。
### 数据库
中V档案馆使用[PostgreSQL](https://postgresql.org)作为数据库,我们承诺定期导出数据库转储 (dump)
文件并公开,其内容遵从以下协议或条款:
- 数据库中的事实性数据根据适用法律不构成受版权保护的内容。中V档案馆放弃一切可能的权利[CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/))。
- 对于数据库中有原创性的内容(如贡献者编辑的描述性内容),如无例外,以[CC BY 4.0协议](https://creativecommons.org/licenses/by/4.0/)提供。
- 对于引用、摘编或改编自萌娘百科、VCPedia的内容以与原始协议(CC BY-NC-SA 3.0
CN)兼容的协议[CC BY-NC-SA 4.0协议](https://creativecommons.org/licenses/by-nc-sa/4.0/)提供,并注明原始协议 。
> 根据原始协议第四条第2项内容CC BY-NC-SA 4.0协议为与原始协议具有相同授权要素的后续版本(“可适用的协议”)。
- 中V档案馆文档使用[CC BY 4.0协议](https://creativecommons.org/licenses/by/4.0/)。
### 软件代码
用于构建中V档案馆的软件代码在[AGPL 3.0](https://www.gnu.org/licenses/agpl-3.0.html)许可证下公开,参见[LICENSE](./LICENSE)
[^1]: 引用自[VCPedia](https://vcpedia.cn/%E9%A6%96%E9%A1%B5),于[知识共享 署名-非商业性使用-相同方式共享 3.0中国大陆 (CC BY-NC-SA 3.0 CN) 许可协议](https://creativecommons.org/licenses/by-nc-sa/3.0/cn/)下提供。
[^2]: 翻译自[VocaDB](https://vocadb.net/),于[CC BY 4.0协议](https://creativecommons.org/licenses/by/4.0/)下提供。
[^3]: 引用自[关于 - 天钿Daily](https://tdd.bunnyxt.com/about)

View File

@ -1,22 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>Astro Basics</title>
</head>
<body>
<slot />
</body>
</html>
---
import "../styles/global.css";
---
<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
}
</style>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CVSA 前端</title>
</head>
<body class="dark:bg-zinc-900 dark:text-zinc-100">
<slot />
</body>
</html>

View File

@ -0,0 +1,10 @@
---
import Layout from '@layouts/Layout.astro';
---
<Layout>
<main class="flex flex-col items-center justify-center min-h-screen gap-8">
<h1 class="text-9xl font-thin">404</h1>
<p class="text-xl font-medium">咦……页面去哪里了(゚Д゚≡゚д゚)!?</p>
</main>
</Layout>

View File

@ -0,0 +1,15 @@
---
import TitleBar from "@components/TitleBar.astro";
import Layout from '@layouts/Layout.astro';
import {Content as AboutContent} from '../content/about.md';
import "../styles/content.css";
---
<Layout>
<TitleBar/>
<main class="flex flex-col items-center min-h-screen gap-8 mt-36 relative z-0">
<div class="lg:w-1/2 content">
<AboutContent/>
</div>
</main>
</Layout>

View File

@ -1,9 +1,6 @@
---
import Welcome from '../components/Welcome.astro';
import Layout from '../layouts/Layout.astro';
// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build
// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
import Welcome from '@components/Welcome.astro';
import Layout from '@layouts/Layout.astro';
---
<Layout>

View File

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

View File

@ -0,0 +1,76 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.content {
@apply text-gray-800 dark:text-zinc-100;
h1,
h2,
h3,
h4 {
@apply font-bold text-gray-900 dark:text-white my-4;
}
h1 {
@apply text-3xl;
}
h2 {
@apply text-2xl;
}
h3 {
@apply text-xl;
}
h4 {
@apply text-lg;
}
p {
@apply my-4;
}
a {
@apply text-blue-500 hover:text-blue-700 dark:hover:text-blue-400 underline;
}
ul,
ol {
@apply list-disc list-inside my-4;
}
li {
@apply my-2;
}
blockquote {
@apply border-l-4 border-gray-300 pl-4 italic my-4;
}
code {
@apply bg-gray-100 text-gray-800 rounded px-1 duration-300;
}
pre {
@apply bg-gray-100 p-4 rounded overflow-x-auto my-4 duration-300 h-0;
}
table {
@apply w-full border-collapse my-4;
}
th,
td {
@apply border border-gray-300 p-2;
}
th {
@apply bg-gray-200 font-bold;
}
ul li p,
ol li p {
@apply inline;
}
}

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: {
extend: {},
},
plugins: [],
};

View File

@ -1,5 +1,16 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@components/*": ["src/components/*"],
"@layouts/*": ["src/layouts/*"],
"@utils/*": ["src/utils/*"],
"@assets/*": ["src/assets/*"],
"@styles": ["src/styles/*"],
"@core/*": ["../core/*"]
}
}
}