From d22fdd43908d1c32812243d2e11619341bab37b0 Mon Sep 17 00:00:00 2001 From: alikia2x Date: Tue, 14 Oct 2025 01:49:56 +0800 Subject: [PATCH] add: some basic info shown in the song info page --- bun.lock | 6 +- packages/elysia/routes/song/info.ts | 76 +++++++++++-- packages/elysia/src/index.ts | 40 +++++-- .../app/components/ui/skeleton.tsx | 13 +++ packages/temp_frontend/app/routes/home.tsx | 4 +- .../app/routes/song/[id]/info.tsx | 106 +++++++++++++++--- packages/temp_frontend/package.json | 2 +- 7 files changed, 209 insertions(+), 38 deletions(-) create mode 100644 packages/temp_frontend/app/components/ui/skeleton.tsx diff --git a/bun.lock b/bun.lock index b147476..69ad9ae 100644 --- a/bun.lock +++ b/bun.lock @@ -197,7 +197,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "isbot": "^5.1.27", - "lucide-react": "^0.544.0", + "lucide-react": "^0.545.0", "next-themes": "^0.4.6", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -1893,7 +1893,7 @@ "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "lucide-react": ["lucide-react@0.544.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw=="], + "lucide-react": ["lucide-react@0.545.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw=="], "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], @@ -3071,6 +3071,8 @@ "plaette/@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="], + "plaette/lucide-react": ["lucide-react@0.544.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw=="], + "plaette/vite": ["vite@7.1.9", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="], "postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], diff --git a/packages/elysia/routes/song/info.ts b/packages/elysia/routes/song/info.ts index 759d438..2be240d 100644 --- a/packages/elysia/routes/song/info.ts +++ b/packages/elysia/routes/song/info.ts @@ -13,7 +13,11 @@ async function getSongIDFromBiliID(id: string) { } else { return null; } - const songID = await dbMain.select({ id: songs.id }).from(songs).where(eq(songs.aid, aid)).limit(1); + 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; } @@ -44,23 +48,29 @@ async function getSingers(id: number) { }) .from(relations) .innerJoin(singer, eq(relations.targetId, singer.id)) - .where(and(eq(relations.sourceId, id), eq(relations.sourceType, "song"), eq(relations.relation, "sing"))); + .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; + async ({ params, status }) => { + const id = params.id; const songID = await getSongID(id); if (!songID) { - return c.status(404, { + return status(404, { message: "song not found" }); } const info = await getSongInfo(songID); if (!info) { - return c.status(404, { + return status(404, { message: "song not found" }); } @@ -70,7 +80,8 @@ export const getSongInfoHandler = new Elysia({ prefix: "/song" }).get( aid: info.aid, producer: info.producer, duration: info.duration, - singers: singers + singers: singers, + cover: info.image || undefined }; }, { @@ -80,7 +91,8 @@ export const getSongInfoHandler = new Elysia({ prefix: "/song" }).get( 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()) + singers: t.Array(t.String()), + cover: t.Optional(t.String()) }), 404: t.Object({ message: t.String() @@ -97,3 +109,51 @@ export const getSongInfoHandler = new Elysia({ prefix: "/song" }).get( } } ); + +export const patchInfoHandler = new Elysia({ prefix: "/song" }).patch( + "/:id/info", + async ({ params, status, body }) => { + const id = params.id; + const songID = await getSongID(id); + if (!songID) { + return status(404, { + message: "song not found" + }); + } + const info = await getSongInfo(songID); + if (!info) { + return status(404, { + message: "song not found" + }); + } + if (body.name) { + await dbMain + .update(songs) + .set({ name: body.name }) + .where(eq(songs.id, songID)); + } + if (body.producer) { + await dbMain + .update(songs) + .set({ producer: body.producer }) + .where(eq(songs.id, songID)); + } + return { + message: "success" + }; + }, + { + response: { + 200: t.Object({ + message: t.String() + }), + 404: t.Object({ + message: t.String() + }) + }, + body: t.Object({ + name: t.Optional(t.String()), + producer: t.Optional(t.String()) + }) + } +); diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index 3b57152..022a366 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -3,7 +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"; +import { getSongInfoHandler, patchInfoHandler } from "@elysia/routes/song/info"; import { rootHandler } from "@elysia/routes/root"; import { getVideoMetadataHandler } from "@elysia/routes/video/metadata"; import { closeMileStoneHandler } from "@elysia/routes/song/milestone"; @@ -19,20 +19,41 @@ const app = new Elysia({ hostname: host } }) - .onAfterHandle({ as: "global" }, ({ responseValue, set, request }) => { + .onAfterHandle({ as: "global" }, ({ responseValue, request }) => { const contentType = request.headers.get("Content-Type") || ""; const accept = request.headers.get("Accept") || ""; const secFetchMode = request.headers.get("Sec-Fetch-Mode"); const requestJson = contentType.includes("application/json"); - const isBrowser = !requestJson && (accept.includes("text/html") || secFetchMode === "navigate"); + const isBrowser = + !requestJson && (accept.includes("text/html") || secFetchMode === "navigate"); const responseValueType = typeof responseValue; const isObject = responseValueType === "object"; - const response = isObject - ? responseValue - : { - message: responseValue - }; - const text = isBrowser ? JSON.stringify(response, null, 2) : JSON.stringify(response); + if (!isObject) { + const response = { + message: responseValue + }; + const text = isBrowser ? JSON.stringify(response, null, 2) : JSON.stringify(response); + return new Response(encoder.encode(text), { + headers: { + "Content-Type": "application/json; charset=utf-8" + } + }); + } + const realResponse = responseValue as Record; + if (realResponse.code) { + const text = isBrowser + ? JSON.stringify(realResponse.response, null, 2) + : JSON.stringify(realResponse.response); + return new Response(encoder.encode(text), { + status: realResponse.code as any, + headers: { + "Content-Type": "application/json; charset=utf-8" + } + }); + } + const text = isBrowser + ? JSON.stringify(realResponse, null, 2) + : JSON.stringify(realResponse); return new Response(encoder.encode(text), { headers: { "Content-Type": "application/json; charset=utf-8" @@ -46,6 +67,7 @@ const app = new Elysia({ .use(getVideoMetadataHandler) .use(getSongInfoHandler) .use(closeMileStoneHandler) + .use(patchInfoHandler) .listen(15412); export const VERSION = "0.7.0"; diff --git a/packages/temp_frontend/app/components/ui/skeleton.tsx b/packages/temp_frontend/app/components/ui/skeleton.tsx new file mode 100644 index 0000000..16d4e12 --- /dev/null +++ b/packages/temp_frontend/app/components/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib//utils" + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/packages/temp_frontend/app/routes/home.tsx b/packages/temp_frontend/app/routes/home.tsx index 1f4c6ae..20eb26a 100644 --- a/packages/temp_frontend/app/routes/home.tsx +++ b/packages/temp_frontend/app/routes/home.tsx @@ -1,11 +1,9 @@ import type { Route } from "./+types/home"; -import { treaty } from "@elysiajs/eden"; -import type { App } from "@elysia/src"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { SearchIcon } from "@/components/icons/search"; -const app = treaty("localhost:15412"); + export function meta({}: Route.MetaArgs) { return [{ title: "中V档案馆" }]; diff --git a/packages/temp_frontend/app/routes/song/[id]/info.tsx b/packages/temp_frontend/app/routes/song/[id]/info.tsx index a3d04ea..62a9397 100644 --- a/packages/temp_frontend/app/routes/song/[id]/info.tsx +++ b/packages/temp_frontend/app/routes/song/[id]/info.tsx @@ -1,29 +1,105 @@ -import useSWR from "swr"; import type { Route } from "./+types/info"; +import { treaty } from "@elysiajs/eden"; +import type { App } from "@elysia/src"; +import { useEffect, useState } from "react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { TriangleAlert } from "lucide-react"; -const API_URL = "https://api.projectcvsa.com"; +const app = treaty(import.meta.env.VITE_API_URL!); + +type SongInfo = Awaited["info"]["get"]>>["data"]; +type SongInfoError = Awaited["info"]["get"]>>["error"]; 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"); - } - return response.json(); - }); + const [data, setData] = useState(null); + const [error, setError] = useState(null); - if (isLoading) return
加载中...
; - if (error) return
错误: {error.message}
; - if (!data) return
暂无数据
; + useEffect(() => { + (async () => { + const { data, error } = await app.song({ id: loaderData.id }).info.get(); + if (error) { + console.log(error); + setError(error); + return; + } + setData(data); + })(); + }, []); + + if (!data && !error) { + return ( +
+
+ +
+ + +
+
+
+ ); + } + + if (error) { + return ( +
+
+
+ +
+

出错了

+

状态码:{error.status}

+ {error.value.message && ( +

+ 错误信息 +
+ {error.value.message} +

+ )} +
+
+ ); + } + + const formatDuration = (duration: number) => { + const minutes = Math.floor(duration / 60); + const seconds = duration % 60; + return `${minutes}:${seconds}`; + }; + + const songNameOnChange = async (e: React.FocusEvent) => { + const name = e.target.textContent; + await app.song({ id: loaderData.id }).info.patch({ name: name || undefined }); + }; return ( -
-

歌曲信息

-
{JSON.stringify(data, null, 2)}
+
+
+ {data!.cover && ( + + )} +
+

+ {data!.name ? data!.name : "未知歌曲名"} +

+
+ + {data!.duration ? formatDuration(data!.duration) : "未知时长"} + + + {data!.producer ? data!.producer : "未知P主"} + +
+
+
); } diff --git a/packages/temp_frontend/package.json b/packages/temp_frontend/package.json index ce9e9d0..630defc 100644 --- a/packages/temp_frontend/package.json +++ b/packages/temp_frontend/package.json @@ -20,7 +20,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "isbot": "^5.1.27", - "lucide-react": "^0.544.0", + "lucide-react": "^0.545.0", "next-themes": "^0.4.6", "react": "^19.1.0", "react-dom": "^19.1.0",