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