add: a endpoint for getting song info
This commit is contained in:
parent
ee29de726e
commit
93237075fe
4
.gitignore
vendored
4
.gitignore
vendored
@ -43,4 +43,6 @@ docker-compose.yml
|
||||
|
||||
ucaptcha-config.yaml
|
||||
|
||||
temp/
|
||||
temp/
|
||||
|
||||
meili
|
||||
@ -1,4 +1,4 @@
|
||||
import { pgTable, uniqueIndex, index, integer, bigint, varchar, text, timestamp, unique, serial, smallint, boolean, bigserial, uuid, pgSequence } from "drizzle-orm/pg-core"
|
||||
import { pgTable, uniqueIndex, index, integer, bigint, varchar, text, timestamp, smallint, boolean, unique, serial, bigserial, uuid, pgSequence } from "drizzle-orm/pg-core"
|
||||
import { sql } from "drizzle-orm"
|
||||
|
||||
|
||||
@ -32,18 +32,6 @@ export const bilibiliMetadata = pgTable("bilibili_metadata", {
|
||||
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
|
||||
@ -104,13 +92,13 @@ export const songs = pgTable("songs", {
|
||||
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(),
|
||||
producer: 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")),
|
||||
@ -122,6 +110,20 @@ export const songs = pgTable("songs", {
|
||||
uniqueIndex("unq_songs_netease_id").using("btree", table.neteaseId.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(),
|
||||
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(),
|
||||
}, (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 singer = pgTable("singer", {
|
||||
id: serial().primaryKey().notNull(),
|
||||
name: text().notNull(),
|
||||
@ -136,12 +138,19 @@ export const relations = pgTable("relations", {
|
||||
targetId: bigint("target_id", { mode: "number" }).notNull(),
|
||||
targetType: text("target_type").notNull(),
|
||||
relation: text().notNull(),
|
||||
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(),
|
||||
}, (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 globalKv = pgTable("global_kv", {
|
||||
key: text().primaryKey().notNull(),
|
||||
value: text().notNull(),
|
||||
});
|
||||
|
||||
export const snapshotSchedule = pgTable("snapshot_schedule", {
|
||||
id: bigserial({ mode: "bigint" }).notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
|
||||
29
packages/elysia/lib/av_bv.ts
Normal file
29
packages/elysia/lib/av_bv.ts
Normal file
@ -0,0 +1,29 @@
|
||||
const XOR_CODE = 23442827791579n;
|
||||
const MASK_CODE = 2251799813685247n;
|
||||
const MAX_AID = 1n << 51n;
|
||||
const BASE = 58n;
|
||||
|
||||
const data = "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf";
|
||||
|
||||
export function av2bv(aid: number) {
|
||||
const bytes = ["B", "V", "1", "0", "0", "0", "0", "0", "0", "0", "0", "0"];
|
||||
let bvIndex = bytes.length - 1;
|
||||
let tmp = (MAX_AID | BigInt(aid)) ^ XOR_CODE;
|
||||
while (tmp > 0) {
|
||||
bytes[bvIndex] = data[Number(tmp % BigInt(BASE))];
|
||||
tmp = tmp / BASE;
|
||||
bvIndex -= 1;
|
||||
}
|
||||
[bytes[3], bytes[9]] = [bytes[9], bytes[3]];
|
||||
[bytes[4], bytes[7]] = [bytes[7], bytes[4]];
|
||||
return bytes.join("") as `BV1${string}`;
|
||||
}
|
||||
|
||||
export function bv2av(bvid: `BV1${string}`) {
|
||||
const bvidArr = Array.from<string>(bvid);
|
||||
[bvidArr[3], bvidArr[9]] = [bvidArr[9], bvidArr[3]];
|
||||
[bvidArr[4], bvidArr[7]] = [bvidArr[7], bvidArr[4]];
|
||||
bvidArr.splice(0, 3);
|
||||
const tmp = bvidArr.reduce((pre, bvidChar) => pre * BASE + BigInt(data.indexOf(bvidChar)), 0n);
|
||||
return Number((tmp & MASK_CODE) ^ XOR_CODE);
|
||||
}
|
||||
92
packages/elysia/routes/song/info.ts
Normal file
92
packages/elysia/routes/song/info.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import { dbMain } from "@core/drizzle";
|
||||
import { relations, singer, songs } from "@core/drizzle/main/schema";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { bv2av } from "@elysia/lib/av_bv";
|
||||
|
||||
async function getSongIDFromBiliID(id: string) {
|
||||
let aid: number;
|
||||
if (id.startsWith("BV1")) {
|
||||
aid = bv2av(id as `BV1${string}`);
|
||||
} else {
|
||||
aid = Number.parseInt(id);
|
||||
}
|
||||
const songID = await dbMain.select({ id: songs.id }).from(songs).where(eq(songs.aid, aid)).limit(1);
|
||||
if (songID.length > 0) {
|
||||
return songID[0].id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getSongID(id: string) {
|
||||
let songID: number | null = null;
|
||||
if (id.startsWith("BV1") || id.startsWith("av")) {
|
||||
const r = await getSongIDFromBiliID(id);
|
||||
r && (songID = r);
|
||||
}
|
||||
if (!songID) {
|
||||
songID = Number.parseInt(id);
|
||||
}
|
||||
return songID;
|
||||
}
|
||||
|
||||
async function getSongInfo(id: number) {
|
||||
const songInfo = await dbMain.select().from(songs).where(eq(songs.id, id)).limit(1);
|
||||
return songInfo[0];
|
||||
}
|
||||
|
||||
async function getSingers(id: number) {
|
||||
const singers = await dbMain
|
||||
.select({
|
||||
singers: singer.name
|
||||
})
|
||||
.from(relations)
|
||||
.innerJoin(singer, eq(relations.targetId, singer.id))
|
||||
.where(and(eq(relations.sourceId, id), eq(relations.sourceType, "song"), eq(relations.relation, "sing")));
|
||||
return singers.map((singer) => singer.singers);
|
||||
}
|
||||
|
||||
export const getSongInfoHandler = new Elysia({ prefix: "/song" }).get(
|
||||
"/:id/info",
|
||||
async (c) => {
|
||||
const id = c.params.id;
|
||||
const songID = await getSongID(id);
|
||||
if (!songID) {
|
||||
return c.status(404, {
|
||||
message: "song not found"
|
||||
});
|
||||
}
|
||||
const info = await getSongInfo(songID);
|
||||
if (!info) {
|
||||
return c.status(404, {
|
||||
message: "song not found"
|
||||
});
|
||||
}
|
||||
const singers = await getSingers(info.id);
|
||||
return {
|
||||
name: info.name,
|
||||
aid: info.aid,
|
||||
producer: info.producer,
|
||||
duration: info.duration,
|
||||
singers: singers
|
||||
};
|
||||
},
|
||||
{
|
||||
response: {
|
||||
200: t.Object({
|
||||
name: t.Union([t.String(), t.Null()]),
|
||||
aid: t.Union([t.Number(), t.Null()]),
|
||||
producer: t.Union([t.String(), t.Null()]),
|
||||
duration: t.Union([t.Number(), t.Null()]),
|
||||
singers: t.Array(t.String())
|
||||
}),
|
||||
404: t.Object({
|
||||
message: t.String()
|
||||
})
|
||||
},
|
||||
detail: {
|
||||
summary: "Get information of a song",
|
||||
description: ""
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -3,6 +3,7 @@ import { getBindingInfo, logStartup } from "./startMessage";
|
||||
import { pingHandler } from "@elysia/routes/ping";
|
||||
import openapi from "@elysiajs/openapi";
|
||||
import { cors } from "@elysiajs/cors";
|
||||
import { getSongInfoHandler } from "@elysia/routes/song/info";
|
||||
|
||||
const [host, port] = getBindingInfo();
|
||||
logStartup(host, port);
|
||||
@ -15,6 +16,7 @@ const app = new Elysia({
|
||||
.use(cors())
|
||||
.use(openapi())
|
||||
.use(pingHandler)
|
||||
.use(getSongInfoHandler)
|
||||
.listen(15412);
|
||||
|
||||
export const VERESION = "0.7.0";
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
import { type RouteConfig, index, route } from "@react-router/dev/routes";
|
||||
|
||||
export default [index("routes/home.tsx"), route("song/[id]/info", "routes/song/[id]/info.tsx")] satisfies RouteConfig;
|
||||
export default [index("routes/home.tsx"), route("song/:id/info", "routes/song/[id]/info.tsx")] satisfies RouteConfig;
|
||||
|
||||
@ -23,26 +23,6 @@ export default function Home() {
|
||||
<SearchIcon className="size-6" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full mt-4 h-10"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const { data } = await app.ping.get("");
|
||||
if (data && data.message === "pong") {
|
||||
toast("pong");
|
||||
return;
|
||||
}
|
||||
toast("校验失败。");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast("发生错误。");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Ping
|
||||
</Button>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import useSWR from "swr";
|
||||
import type { Route } from "./+types/info";
|
||||
|
||||
const API_URL = "https://api.projectcvsa.com";
|
||||
|
||||
export default function SongInfo() {
|
||||
const { data, error, isLoading } = useSWR(`${API_URL}/song/[id]/info`, async (url) => {
|
||||
export async function clientLoader({ params }: Route.LoaderArgs) {
|
||||
return { id: params.id };
|
||||
}
|
||||
|
||||
export default function SongInfo({ loaderData }: Route.ComponentProps) {
|
||||
const { data, error, isLoading } = useSWR(`${API_URL}/video/${loaderData.id}/info`, async (url) => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch song info");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user