feat: tsy过滤器

This commit is contained in:
mei 2025-02-10 12:21:25 +08:00
parent 32db048ad5
commit 5df0031a63
4 changed files with 187 additions and 225 deletions

43
app/blocked/page.tsx Normal file
View File

@ -0,0 +1,43 @@
"use client"
import { useEffect, useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import Link from "next/link"
export default function BlockedPage() {
const [countdown, setCountdown] = useState(3)
useEffect(() => {
const timer = setInterval(() => {
setCountdown((prev) => prev - 1)
}, 1000)
const redirect = setTimeout(() => {
window.location.href = "https://www.bilibili.com/video/BV1kW411m7VP"
}, 3000)
return () => {
clearInterval(timer)
clearTimeout(redirect)
}
}, [])
return (
<div className="container mx-auto px-4 py-8">
<Card className="bg-red-100">
<CardHeader>
<CardTitle className="text-2xl font-bold text-red-600">访</CardTitle>
</CardHeader>
<CardContent>
<p className="text-red-500 mb-4">访</p>
<p className="text-gray-600 mb-4"></p>
<p className="text-gray-600 mb-4">{countdown} ...</p>
<Link href="/" className="text-blue-500 hover:underline">
</Link>
</CardContent>
</Card>
</div>
)
}

View File

@ -1,10 +1,12 @@
"use client" "use client"
import { useState } from "react" import { useState } from "react"
import { useRouter } from "next/navigation"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Loader2 } from "lucide-react" import { Loader2 } from "lucide-react"
import { blockedUploaderIds } from "@/config/blocked-ids"
interface Uploader { interface Uploader {
mid: string mid: string
@ -19,6 +21,7 @@ export default function UploaderSearch() {
const [uploaders, setUploaders] = useState<Uploader[]>([]) const [uploaders, setUploaders] = useState<Uploader[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const router = useRouter()
const handleSearch = async () => { const handleSearch = async () => {
if (!uid) { if (!uid) {
@ -26,15 +29,21 @@ export default function UploaderSearch() {
return return
} }
// 检查是否为禁止的 UP 主 ID
if (blockedUploaderIds.includes(uid)) {
router.push("/blocked")
return
}
setLoading(true) setLoading(true)
setError(null) setError(null)
try { try {
const response = await fetch(`https://ecs-113-44-166-103.compute.hwclouds-dns.com/basic/v1/uploader/${uid}`) const response = await fetch(`https://api.ninevocalrank.top/basic/v1/uploader/${uid}`)
if (!response.ok) { if (!response.ok) {
throw new Error("服务器响应错误") throw new Error("服务器响应错误")
} }
const data: Uploader = await response.json() const data: Uploader = await response.json()
setUploaders([data]) // 将单个 UP 主数据包装在数组中 setUploaders([data])
} catch (error) { } catch (error) {
console.error("搜索UP主时出错:", error) console.error("搜索UP主时出错:", error)
setError("搜索过程中出现错误") setError("搜索过程中出现错误")

View File

@ -1,141 +1,125 @@
"use client"; "use client"
import { useState } from "react"; import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { useRouter } from "next/navigation"
import { Input } from "@/components/ui/input"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"
import { Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"
// import { import { Loader2 } from "lucide-react"
// Dialog, import { blockedVideoIds } from "@/config/blocked-ids"
// DialogContent,
// DialogDescription,
// DialogHeader,
// DialogTitle,
// DialogFooter,
// } from "@/components/ui/dialog";
interface Video { interface Video {
video_stat: { video_stat: {
view: number; view: number
like: number; like: number
coin: number; coin: number
favorite: number; favorite: number
reply: number; reply: number
share: number; share: number
danmaku: number; danmaku: number
}; }
video_info: { video_info: {
uploader_mid: string; uploader_mid: string
uploader_name: string; uploader_name: string
title: string; title: string
pic: string; pic: string
pages: number; pages: number
timestamp: number; timestamp: number
}; }
video_id: { video_id: {
avid: string; avid: string
bvid: string; bvid: string
}; }
vrank_info: { vrank_info: {
vrank_score: number; vrank_score: number
rank: string; rank: string
rank_code: number; rank_code: number
progress_percentage: number; progress_percentage: number
}; }
video_increase: { video_increase: {
view: number; view: number
like: number; like: number
coin: number; coin: number
favorite: number; favorite: number
reply: number; reply: number
share: number; share: number
danmaku: number; danmaku: number
}; }
score_rank: number; score_rank: number
} }
function getAchievement(views: number) { function getAchievement(views: number) {
if (views >= 10000000) return { name: "神话曲", next: null, progress: 100 }; if (views >= 10000000) return { name: "神话曲", next: null, progress: 100 }
if (views >= 1000000) if (views >= 1000000)
return { return {
name: "传说曲", name: "传说曲",
next: "神话曲", next: "神话曲",
progress: (views / 10000000) * 100, progress: (views / 10000000) * 100,
}; }
if (views >= 100000) if (views >= 100000)
return { return {
name: "殿堂曲", name: "殿堂曲",
next: "传说曲", next: "传说曲",
progress: (views / 1000000) * 100, progress: (views / 1000000) * 100,
}; }
return { name: "未达成", next: "殿堂曲", progress: (views / 100000) * 100 }; return { name: "未达成", next: "殿堂曲", progress: (views / 100000) * 100 }
} }
export default function VideoSearch() { export default function VideoSearch() {
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("")
const [videos, setVideos] = useState<Video[]>([]); const [videos, setVideos] = useState<Video[]>([])
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null)
// const [showCoverDialog, setShowCoverDialog] = useState(false); const router = useRouter()
// const [confirmText, setConfirmText] = useState("");
// const [showCover, setShowCover] = useState(false);
const handleSearch = async () => { const handleSearch = async () => {
if (!searchTerm) { if (!searchTerm) {
setError("请输入搜索内容"); setError("请输入搜索内容")
return; return
} }
setLoading(true); // 检查是否为禁止的视频 ID
setError(null); if (blockedVideoIds.includes(searchTerm)) {
router.push("/blocked")
return
}
setLoading(true)
setError(null)
try { try {
const response = await fetch( const response = await fetch(
`https://ecs-113-44-166-103.compute.hwclouds-dns.com/vocaloid_rank/v1/video/${searchTerm}` `https://ecs-113-44-166-103.compute.hwclouds-dns.com/vocaloid_rank/v1/video/${searchTerm}`,
); )
const weekly_response = await fetch( const weekly_response = await fetch(
`https://ecs-113-44-166-103.compute.hwclouds-dns.com/vocaloid_rank/v1/sorted/${searchTerm}` `https://ecs-113-44-166-103.compute.hwclouds-dns.com/vocaloid_rank/v1/sorted/${searchTerm}`,
); )
if (!response.ok || !weekly_response.ok) { if (!response.ok || !weekly_response.ok) {
throw new Error("服务器响应错误"); throw new Error("服务器响应错误")
} }
const data = await response.json(); const data = await response.json()
const weekly_data = await weekly_response.json(); const weekly_data = await weekly_response.json()
const combinedData = { const combinedData = {
...data, ...data,
score_rank: weekly_data.score_rank, score_rank: weekly_data.score_rank,
}; }
setVideos([combinedData]); setVideos([combinedData])
// setShowCover(false);
if (videos.length === 0) { if (videos.length === 0) {
setError("未找到匹配的视频"); setError("未找到匹配的视频")
} }
} catch (error) { } catch (error) {
console.error("搜索视频时出错:", error); console.error("搜索视频时出错:", error)
setError("搜索过程中出现错误"); setError("搜索过程中出现错误")
} finally { } finally {
setLoading(false); setLoading(false)
} }
}; }
// const handleShowCover = () => {
// setShowCoverDialog(true);
// };
// const handleConfirmShowCover = () => {
// if (confirmText === "我已知晓") {
// setShowCover(true);
// setShowCoverDialog(false);
// }
// };
return ( return (
<Card className="bg-gradient-to-br from-yellow-100 to-red-100"> <Card className="bg-gradient-to-br from-yellow-100 to-red-100">
<CardHeader> <CardHeader>
<CardTitle className="text-2xl font-bold text-primary"> <CardTitle className="text-2xl font-bold text-primary"></CardTitle>
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex space-x-2 mb-4"> <div className="flex space-x-2 mb-4">
@ -153,74 +137,32 @@ export default function VideoSearch() {
{videos.length > 0 ? ( {videos.length > 0 ? (
<ul className="space-y-8"> <ul className="space-y-8">
{videos.map((video) => { {videos.map((video) => {
const achievement = getAchievement(video.video_stat.view); const achievement = getAchievement(video.video_stat.view)
return ( return (
<li <li key={video.video_id.bvid} className="border p-6 rounded-lg bg-white shadow-md">
key={video.video_id.bvid}
className="border p-6 rounded-lg bg-white shadow-md"
>
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{/* 此功能可能引发版权纠纷,不再使用 */}
{/* <div className="w-full">
{showCover ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={video.video_info.pic || "/placeholder.svg"}
className="w-full h-64 object-cover rounded-lg"
alt="视频封面"
/>
) : (
<Button onClick={handleShowCover}></Button>
)}
</div> */}
<div className="w-full"> <div className="w-full">
<h3 className="font-bold text-xl mb-2"> <h3 className="font-bold text-xl mb-2">{video.video_info.title}</h3>
{video.video_info.title}
</h3>
<p className="text-gray-600"> <p className="text-gray-600">
UP主: {video.video_info.uploader_name} (UID:{" "} UP主: {video.video_info.uploader_name} (UID: {video.video_info.uploader_mid})
{video.video_info.uploader_mid})
</p>
<p className="text-gray-600">
BV号: {video.video_id.bvid}
</p>
<p className="text-gray-600">
AV号: {video.video_id.avid}
</p> </p>
<p className="text-gray-600">BV号: {video.video_id.bvid}</p>
<p className="text-gray-600">AV号: {video.video_id.avid}</p>
<div className="mt-4 grid grid-cols-2 gap-2"> <div className="mt-4 grid grid-cols-2 gap-2">
<p className="text-gray-600"> <p className="text-gray-600">: {video.video_stat.view.toLocaleString()}</p>
: {video.video_stat.view.toLocaleString()} <p className="text-gray-600">: {video.video_stat.like.toLocaleString()}</p>
</p> <p className="text-gray-600">: {video.video_stat.coin.toLocaleString()}</p>
<p className="text-gray-600"> <p className="text-gray-600">: {video.video_stat.favorite.toLocaleString()}</p>
: {video.video_stat.like.toLocaleString()} <p className="text-gray-600">: {video.video_stat.reply.toLocaleString()}</p>
</p> <p className="text-gray-600">: {video.video_stat.share.toLocaleString()}</p>
<p className="text-gray-600"> <p className="text-gray-600">: {video.video_stat.danmaku.toLocaleString()}</p>
: {video.video_stat.coin.toLocaleString()}
</p>
<p className="text-gray-600">
: {video.video_stat.favorite.toLocaleString()}
</p>
<p className="text-gray-600">
: {video.video_stat.reply.toLocaleString()}
</p>
<p className="text-gray-600">
: {video.video_stat.share.toLocaleString()}
</p>
<p className="text-gray-600">
: {video.video_stat.danmaku.toLocaleString()}
</p>
</div> </div>
<div className="mt-4 bg-blue-100 p-4 rounded-lg"> <div className="mt-4 bg-blue-100 p-4 rounded-lg">
<h4 className="font-bold text-lg mb-2"></h4> <h4 className="font-bold text-lg mb-2"></h4>
{video.vrank_info ? ( {video.vrank_info ? (
<> <>
<p className="text-gray-700"> <p className="text-gray-700">: {video.vrank_info.vrank_score.toFixed(2)}</p>
:{" "} <p className="text-gray-700 font-bold text-xl">: {video.score_rank}</p>
{video.vrank_info.vrank_score.toFixed(2)}
</p>
<p className="text-gray-700 font-bold text-xl">
: {video.score_rank}
</p>
</> </>
) : ( ) : (
<p className="text-gray-700"></p> <p className="text-gray-700"></p>
@ -229,48 +171,24 @@ export default function VideoSearch() {
<> <>
<h5 className="font-semibold mt-2"></h5> <h5 className="font-semibold mt-2"></h5>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<p className="text-gray-700">: {video.video_increase.view.toLocaleString()}</p>
<p className="text-gray-700">: {video.video_increase.like.toLocaleString()}</p>
<p className="text-gray-700">: {video.video_increase.coin.toLocaleString()}</p>
<p className="text-gray-700"> <p className="text-gray-700">
:{" "} : {video.video_increase.favorite.toLocaleString()}
{video.video_increase.view.toLocaleString()}
</p>
<p className="text-gray-700">
:{" "}
{video.video_increase.like.toLocaleString()}
</p>
<p className="text-gray-700">
:{" "}
{video.video_increase.coin.toLocaleString()}
</p>
<p className="text-gray-700">
:{" "}
{video.video_increase.favorite.toLocaleString()}
</p>
<p className="text-gray-700">
:{" "}
{video.video_increase.reply.toLocaleString()}
</p>
<p className="text-gray-700">
:{" "}
{video.video_increase.share.toLocaleString()}
</p>
<p className="text-gray-700">
:{" "}
{video.video_increase.danmaku.toLocaleString()}
</p> </p>
<p className="text-gray-700">: {video.video_increase.reply.toLocaleString()}</p>
<p className="text-gray-700">: {video.video_increase.share.toLocaleString()}</p>
<p className="text-gray-700">: {video.video_increase.danmaku.toLocaleString()}</p>
</div> </div>
</> </>
)} )}
</div> </div>
<p className="text-gray-600 mt-2"> <p className="text-gray-600 mt-2">
:{" "} : {new Date(video.video_info.timestamp * 1000).toLocaleString("zh-CN")}
{new Date(
video.video_info.timestamp * 1000
).toLocaleString("zh-CN")}
</p> </p>
<div className="mt-4"> <div className="mt-4">
<p className="font-semibold"> <p className="font-semibold">: {achievement.name}</p>
: {achievement.name}
</p>
{achievement.next && ( {achievement.next && (
<div className="mt-2"> <div className="mt-2">
<p> <p>
@ -278,8 +196,8 @@ export default function VideoSearch() {
{(achievement.next === "殿堂曲" {(achievement.next === "殿堂曲"
? 100000 ? 100000
: achievement.next === "传说曲" : achievement.next === "传说曲"
? 1000000 ? 1000000
: 10000000) - video.video_stat.view}{" "} : 10000000) - video.video_stat.view}{" "}
</p> </p>
<div className="w-full bg-gray-200 rounded-full h-2.5 mt-2"> <div className="w-full bg-gray-200 rounded-full h-2.5 mt-2">
@ -294,52 +212,14 @@ export default function VideoSearch() {
</div> </div>
</div> </div>
</li> </li>
); )
})} })}
</ul> </ul>
) : ( ) : (
!loading && !error && <p></p> !loading && !error && <p></p>
)} )}
</CardContent> </CardContent>
{/* <Dialog open={showCoverDialog} onOpenChange={setShowCoverDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
API获取内容引用使
<ul>
<li>
访
<a href="https://www.bilibili.com/video"></a>
</li>
<li>
i@mei.lv
</li>
<li>
</li>
<li></li>
<li>
使
</li>
</ul>
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<Input
placeholder="请输入'我已知晓'以确认"
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
/>
</div>
<DialogFooter>
<Button onClick={handleConfirmShowCover}></Button>
</DialogFooter>
</DialogContent>
</Dialog> */}
</Card> </Card>
); )
} }

30
config/blocked-ids.ts Normal file
View File

@ -0,0 +1,30 @@
// tsy 过滤器
export const blockedVideoIds = [
"BV1Nmp4eKE5H",
"BV1M5fDYkEGB",
"BV1hqykY9E72",
"BV1LHU9YuEqm",
"BV1AtidYfES6",
"BV1TLkVYbEsS",
"BV1iE421w7B6",
"BV1mdW8enEdh",
"BV1m9cbepErC",
"BV1Xpr6YyErN",
"BV1PAFWeFE3f",
"BV1wYwTeHEEW",
"BV1cMSUYiE5m",
"BV157yLYXEZN",
"BV1h2tue6EbD",
"BV1MSYMeEEZS",
"BV1Nmp4eKE5H",
"BV1zBY2euEA4",
"BV1bStreZEMr",
"BV1dt2eYyE86",
"BV1M8411R75Z",
]
export const blockedUploaderIds = [
"1037289255",
"823284"
]