1
0

add: some real data fetched from database for the song info page to replace placeholders

This commit is contained in:
alikia2x (寒寒) 2025-09-08 01:23:56 +08:00
parent 29b924de5d
commit 2358229ca8
11 changed files with 484 additions and 550 deletions

4
.gitignore vendored
View File

@ -41,4 +41,6 @@ build/
docker-compose.yml
ucaptcha-config.yaml
ucaptcha-config.yaml
temp/

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" />

View File

@ -6,5 +6,6 @@ export default defineConfig({
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL_MAIN!
}
},
tablesFilter: ["*"],
});

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

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

View File

@ -1,316 +1,174 @@
import {
pgTable,
index,
text,
timestamp,
unique,
serial,
bigint,
integer,
uniqueIndex,
varchar,
uuid,
smallint,
bigserial,
boolean,
interval,
real,
pgSequence
} from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
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 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 content = pgTable(
"content",
{
pageId: text("page_id").primaryKey().notNull(),
pageContent: text("page_content").notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }),
deletedAt: timestamp("deleted_at", { withTimezone: true, mode: "string" })
},
(table) => [index("idx_content_created-at").using("btree", table.createdAt.asc().nullsLast().op("timestamptz_ops"))]
);
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 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 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 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 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"))
]
);
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_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 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 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()
},
(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 viewsIncrementRate = pgTable(
"views_increment_rate",
{
id: integer()
.default(sql`nextval('views_increment_rate_id_seq'::regclass)`)
.notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
aid: bigint({ mode: "number" }).notNull(),
oldTime: timestamp("old_time", { withTimezone: true, mode: "string" }).notNull(),
newTime: timestamp("new_time", { withTimezone: true, mode: "string" }).notNull(),
oldViews: integer("old_views").notNull(),
newViews: integer("new_views").notNull(),
interval: interval().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
speed: real()
},
(table) => [
uniqueIndex("unq_views-increment-rate_aid_interval").using(
"btree",
table.aid.asc().nullsLast().op("int8_ops"),
table.interval.asc().nullsLast().op("int8_ops")
),
uniqueIndex("views_increment_rate_pkey").using("btree", table.id.asc().nullsLast().op("int4_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

@ -1,9 +1,10 @@
import type { InferSelectModel } from "drizzle-orm";
import { users } from "~db/cred/schema";
import { bilibiliMetadata, latestVideoSnapshot, videoSnapshot } from "~db/main/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

@ -1,246 +1,63 @@
import { Layout } from "~/components/layout";
import { Button, Card, CardContent, CardMedia, ExtendedFAB, IconButton, Typography } from "@m3-components/solid";
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";
import { TabSwitcher } from "~/components/song/TabSwitcher";
import { EditIcon, HomeIcon, MusicIcon } from "~/components/icons";
import { A } from "@solidjs/router";
import { RightArrow } from "~/components/icons/Arrow";
import { Component } from "solid-js";
import { LinkIcon } from "~/components/icons/Link";
import { StarBadge4, StarBadge6, StarBadge8 } from "~/components/icons/StarBadges";
import { HistoryIcon } from "~/components/icons/History";
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>
);
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 Content: Component = () => {
return (
<>
<Card variant="outlined" class="w-full max-lg:rounded-none max-lg:border-none">
<div class="relative w-full overflow-hidden ">
<CardMedia
round={false}
src="https://i0.hdslb.com/bfs/archive/8ad220336f96e4d2ea05baada3bc04592d56b2a5.jpg"
referrerpolicy="no-referrer"
class="relative w-full z-[2]"
/>
<div class="h-10 lg:h-0" />
<CardMedia
round={false}
src="https://i0.hdslb.com/bfs/archive/8ad220336f96e4d2ea05baada3bc04592d56b2a5.jpg"
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>
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;
}
<CardContent class="max-lg:hidden px-7 py-6 flex flex-col gap-4">
<Typography.Display class="leading-[2.75rem]" variant="small">
</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>
</>
);
};
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 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 />
</>
);
};
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;
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="/singer/赤羽/songs">
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
<LinkIcon class="w-5 h-5 text-xl" />
<span></span>
</Button>
</A>
<A href="/songs">
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
<HistoryIcon class="w-5 h-5 text-xl" />
<span></span>
</Button>
</A>
</div>
</>
);
};
export default function Info() {
const params = useParams();
const info = createAsync(() => getSongInfoFromID(params.id));
return (
<Layout>
<title> - - V </title>
@ -252,7 +69,7 @@ export default function Info() {
<LeftSideBar />
</nav>
<main class="mb-24">
<Content />
<Content data={info() || null}/>
</main>
<div class="top-32 hidden lg:flex self-start sticky flex-col pb-12 px-6">
<RightSideBar />