diff --git a/Vocal-rank.code-workspace b/Vocal-rank.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/Vocal-rank.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/components/uploader-search.tsx b/components/uploader-search.tsx index c2e8e71..28fd28d 100644 --- a/components/uploader-search.tsx +++ b/components/uploader-search.tsx @@ -29,7 +29,7 @@ export default function UploaderSearch() { return } - // 检查是否为禁止的 UP 主 ID + // tsy 过滤器 if (blockedUploaderIds.includes(uid)) { router.push("/blocked") return diff --git a/components/video-search.tsx b/components/video-search.tsx index 13e06d1..2372e72 100644 --- a/components/video-search.tsx +++ b/components/video-search.tsx @@ -1,131 +1,275 @@ -"use client" +"use client"; -import { useState } from "react" -import { useRouter } from "next/navigation" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { Button } from "@/components/ui/button" -import { Loader2 } from "lucide-react" -import { blockedVideoIds } from "@/config/blocked-ids" +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Loader2 } from "lucide-react"; +import { blockedVideoIds } from "@/config/blocked-ids"; interface Video { video_stat: { - view: number - like: number - coin: number - favorite: number - reply: number - share: number - danmaku: number - } + view: number; + like: number; + coin: number; + favorite: number; + reply: number; + share: number; + danmaku: number; + }; video_info: { - uploader_mid: string - uploader_name: string - title: string - pic: string - pages: number - timestamp: number - } + uploader_mid: string; + uploader_name: string; + title: string; + pic: string; + pages: number; + timestamp: number; + }; video_id: { - avid: string - bvid: string - } + avid: string; + bvid: string; + }; vrank_info: { - vrank_score: number - rank: string - rank_code: number - progress_percentage: number - } + vrank_score: number; + rank: string; + rank_code: number; + progress_percentage: number; + }; video_increase: { - view: number - like: number - coin: number - favorite: number - reply: number - share: number - danmaku: number - } - score_rank: number + view: number; + like: number; + coin: number; + favorite: number; + reply: number; + share: number; + danmaku: number; + }; + score_rank: number; } function getAchievement(views: number) { - if (views >= 10000000) return { name: "神话曲", next: null, progress: 100 } - if (views >= 1000000) + const safeViews = Math.max(0, Number(views) || 0); + if (safeViews >= 10000000) + return { name: "神话曲", next: null, progress: 100 }; + if (safeViews >= 1000000) return { name: "传说曲", next: "神话曲", - progress: (views / 10000000) * 100, - } - if (views >= 100000) + progress: (safeViews / 10000000) * 100, + }; + if (safeViews >= 100000) return { name: "殿堂曲", next: "传说曲", - progress: (views / 1000000) * 100, - } - return { name: "未达成", next: "殿堂曲", progress: (views / 100000) * 100 } + progress: (safeViews / 1000000) * 100, + }; + return { + name: "未达成", + next: "殿堂曲", + progress: (safeViews / 100000) * 100, + }; } +const VideoInfo = ({ video }: { video: Video }) => { + const achievement = getAchievement(video.video_stat.view); + + return ( +
  • +
    +
    +

    {video.video_info.title}

    +

    + UP主: {video.video_info.uploader_name} (UID:{" "} + {video.video_info.uploader_mid}) +

    +

    BV号: {video.video_id.bvid}

    +

    AV号: {video.video_id.avid}

    +
    +

    + 播放量: {video.video_stat.view.toLocaleString()} +

    +

    + 点赞数: {video.video_stat.like.toLocaleString()} +

    +

    + 投币: {video.video_stat.coin.toLocaleString()} +

    +

    + 收藏: {video.video_stat.favorite.toLocaleString()} +

    +

    + 评论: {video.video_stat.reply.toLocaleString()} +

    +

    + 分享: {video.video_stat.share.toLocaleString()} +

    +

    + 弹幕数: {video.video_stat.danmaku.toLocaleString()} +

    +
    +
    +

    周刊数据

    + {video.vrank_info ? ( + <> +

    + 周刊得分: {video.vrank_info.vrank_score.toFixed(2)} +

    +

    + 周刊排名: {video.score_rank} +

    + + ) : ( +

    暂无周刊数据

    + )} + {video.video_increase && ( + <> +
    数据增长:
    +
    +

    + 播放增长: {video.video_increase.view.toLocaleString()} +

    +

    + 点赞增长: {video.video_increase.like.toLocaleString()} +

    +

    + 投币增长: {video.video_increase.coin.toLocaleString()} +

    +

    + 收藏增长: {video.video_increase.favorite.toLocaleString()} +

    +

    + 评论增长: {video.video_increase.reply.toLocaleString()} +

    +

    + 分享增长: {video.video_increase.share.toLocaleString()} +

    +

    + 弹幕增长: {video.video_increase.danmaku.toLocaleString()} +

    +
    + + )} +
    +

    + 数据更新时间:{" "} + {new Date(video.video_info.timestamp * 1000).toLocaleString( + "zh-CN" + )} +

    +
    +

    成就: {achievement.name}

    + {achievement.next && ( +
    +

    + 距离 {achievement.next} 还需{" "} + {(achievement.next === "殿堂曲" + ? 100000 + : achievement.next === "传说曲" + ? 1000000 + : 10000000) - video.video_stat.view}{" "} + 播放 +

    +
    +
    +
    +
    + )} +
    +
    +
    +
  • + ); +}; + export default function VideoSearch() { - const [searchTerm, setSearchTerm] = useState("") - const [videos, setVideos] = useState([]) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const router = useRouter() + const [searchTerm, setSearchTerm] = useState(""); + const [videos, setVideos] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const router = useRouter(); const handleSearch = async () => { - if (!searchTerm) { - setError("请输入搜索内容") - return + if (!searchTerm.trim()) { + setError("请输入有效的搜索内容"); + return; + } + + // 输入格式验证 + if (!/^(BV([a-zA-Z0-9]{10})|AV\d+)$/.test(searchTerm)) { + setError("请输入有效的BV号或AV号"); + return; } - // 检查是否为禁止的视频 ID if (blockedVideoIds.includes(searchTerm)) { - router.push("/blocked") - return + router.push("/blocked"); + return; } - setLoading(true) - setError(null) + setLoading(true); + setError(null); try { - const response = await fetch( - `https://api.mmeiblog.cn/NineVocalRank/vocaloid_rank/v1/video/${searchTerm}`, - ) - const weekly_response = await fetch( - `https://api.mmeiblog.cn/NineVocalRank/vocaloid_rank/v1/sorted/${searchTerm}`, - ) - if (!response.ok || !weekly_response.ok) { - throw new Error("服务器响应错误") + const [response, weeklyResponse] = await Promise.all([ + fetch( + `https://api.mmeiblog.cn/NineVocalRank/vocaloid_rank/v1/video/${searchTerm}` + ), + fetch( + `https://api.mmeiblog.cn/NineVocalRank/vocaloid_rank/v1/sorted/${searchTerm}` + ), + ]); + + if (!response.ok || !weeklyResponse.ok) { + const statusText = + response.status === 404 ? "视频未找到" : "服务器响应错误"; + throw new Error(statusText); } - const data = await response.json() - const weekly_data = await weekly_response.json() + + const data = await response.json(); + const weeklyData = await weeklyResponse.json(); const combinedData = { ...data, - score_rank: weekly_data.score_rank, - } + score_rank: weeklyData.score_rank, + }; - setVideos([combinedData]) - if (videos.length === 0) { - setError("未找到匹配的视频") + setVideos([combinedData]); + setLoading(false); + if (!combinedData) { + setError("未找到匹配的视频"); } - } catch (error) { - console.error("搜索视频时出错:", error) - setError("搜索过程中出现错误") - } finally { - setLoading(false) + } catch (err) { + if (err instanceof TypeError && err.message.includes("network error")) { + setError("网络连接异常,请检查网络状态"); + } else if (err instanceof Error && err.message.includes("JSON")) { + setError("数据解析失败,请稍后重试"); + } else if (err instanceof Error && err.message.includes("视频不符合")) { + setError("该视频不在Vocaloid分区,无法查询"); + } else { + setError(err instanceof Error ? err.message : "搜索过程中出现未知错误"); + } + console.error("搜索视频时出错:", err); + setLoading(false); } - } + }; return ( - 搜索视频 + + 搜索视频 +
    setSearchTerm(e.target.value)} /> @@ -136,90 +280,21 @@ export default function VideoSearch() { {error &&

    {error}

    } {videos.length > 0 ? (
      - {videos.map((video) => { - const achievement = getAchievement(video.video_stat.view) - return ( -
    • -
      -
      -

      {video.video_info.title}

      -

      - UP主: {video.video_info.uploader_name} (UID: {video.video_info.uploader_mid}) -

      -

      BV号: {video.video_id.bvid}

      -

      AV号: {video.video_id.avid}

      -
      -

      播放量: {video.video_stat.view.toLocaleString()}

      -

      点赞数: {video.video_stat.like.toLocaleString()}

      -

      投币: {video.video_stat.coin.toLocaleString()}

      -

      收藏: {video.video_stat.favorite.toLocaleString()}

      -

      评论: {video.video_stat.reply.toLocaleString()}

      -

      分享: {video.video_stat.share.toLocaleString()}

      -

      弹幕数: {video.video_stat.danmaku.toLocaleString()}

      -
      -
      -

      周刊数据

      - {video.vrank_info ? ( - <> -

      周刊得分: {video.vrank_info.vrank_score.toFixed(2)}

      -

      周刊排名: {video.score_rank}

      - - ) : ( -

      暂无周刊数据

      - )} - {video.video_increase && ( - <> -
      数据增长:
      -
      -

      播放增长: {video.video_increase.view.toLocaleString()}

      -

      点赞增长: {video.video_increase.like.toLocaleString()}

      -

      投币增长: {video.video_increase.coin.toLocaleString()}

      -

      - 收藏增长: {video.video_increase.favorite.toLocaleString()} -

      -

      评论增长: {video.video_increase.reply.toLocaleString()}

      -

      分享增长: {video.video_increase.share.toLocaleString()}

      -

      弹幕增长: {video.video_increase.danmaku.toLocaleString()}

      -
      - - )} -
      -

      - 数据更新时间: {new Date(video.video_info.timestamp * 1000).toLocaleString("zh-CN")} -

      -
      -

      成就: {achievement.name}

      - {achievement.next && ( -
      -

      - 距离 {achievement.next} 还需{" "} - {(achievement.next === "殿堂曲" - ? 100000 - : achievement.next === "传说曲" - ? 1000000 - : 10000000) - video.video_stat.view}{" "} - 播放 -

      -
      -
      -
      -
      - )} -
      -
      -
      -
    • - ) - })} + {videos + .map((video) => { + // 空值校验 + if (!video?.video_id?.bvid) { + console.warn("该视频不在Vocaloid分区,无法查询", video); + return null; + } + return ; + }) + .filter(Boolean)}
    ) : ( !loading && !error &&

    暂无数据

    )} - ) + ); } -