/* eslint-disable react-hooks/exhaustive-deps */ "use client"; import { useMemo, useState } from "react"; type FormatInfo = { format_id: string; ext?: string | null; resolution?: string | null; fps?: number | null; vcodec?: string | null; acodec?: string | null; filesize?: number | null; filesize_approx?: number | null; }; type VideoInfo = { id: string; title: string; duration?: number | null; uploader?: string | null; thumbnail?: string | null; webpage_url?: string | null; formats: FormatInfo[]; }; type DownloadResponse = { file_name: string; download_url: string; }; type DownloadState = | { status: "idle" } | { status: "loading" } | { status: "success"; url: string; file: string } | { status: "error"; message: string }; const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8000"; const DEFAULT_DOWNLOAD_KEY = "__default"; function formatFileSize(bytes?: number | null) { if (!bytes || bytes <= 0) return "未知"; const units = ["B", "KB", "MB", "GB", "TB"]; let value = bytes; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024; unitIndex += 1; } return `${value.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`; } function formatDuration(seconds?: number | null) { if (!seconds || seconds <= 0) return null; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); if (h > 0) { return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; } return `${m}:${String(s).padStart(2, "0")}`; } export default function Home() { const [url, setUrl] = useState(""); const [videoInfo, setVideoInfo] = useState(null); const [infoState, setInfoState] = useState< "idle" | "loading" | "success" | "error" >("idle"); const [infoError, setInfoError] = useState(null); const [downloads, setDownloads] = useState>({}); const baseDownloadState = useMemo( () => ({ status: "idle" }), [] ); const sortedFormats = useMemo(() => { if (!videoInfo) return []; if (!videoInfo.formats?.length) return []; const byQuality = [...videoInfo.formats].sort((a, b) => { const sizeA = a.filesize ?? a.filesize_approx ?? 0; const sizeB = b.filesize ?? b.filesize_approx ?? 0; if (sizeA !== sizeB) return sizeB - sizeA; const resA = (a.resolution ?? "").match(/\d+/g)?.map(Number) ?? [0, 0]; const resB = (b.resolution ?? "").match(/\d+/g)?.map(Number) ?? [0, 0]; if (resA[0] !== resB[0]) return resB[0] - resA[0]; if (resA[1] !== resB[1]) return resB[1] - resA[1]; return (b.fps ?? 0) - (a.fps ?? 0); }); return byQuality.filter( (format, index, array) => array.findIndex((item) => item.format_id === format.format_id) === index ); }, [videoInfo]); const handleFetchInfo = async (event: React.FormEvent) => { event.preventDefault(); if (!url.trim()) { setInfoError("请输入有效的视频链接。"); setInfoState("error"); return; } try { setInfoState("loading"); setInfoError(null); setVideoInfo(null); setDownloads({}); const response = await fetch( `${API_BASE}/api/info?url=${encodeURIComponent(url.trim())}` ); if (!response.ok) { const detail = await response.text(); throw new Error( detail || `获取失败,状态码 ${response.status.toString()}` ); } const info = (await response.json()) as VideoInfo; setVideoInfo(info); setInfoState("success"); } catch (error) { const message = error instanceof Error ? error.message : "获取视频信息失败。"; setInfoError(message); setInfoState("error"); } }; const startDownload = (formatId: string | null) => { const key = formatId ?? DEFAULT_DOWNLOAD_KEY; setDownloads((prev) => ({ ...prev, [key]: { status: "loading" }, })); }; const finishDownload = ( formatId: string | null, response: DownloadResponse ) => { const key = formatId ?? DEFAULT_DOWNLOAD_KEY; setDownloads((prev) => ({ ...prev, [key]: { status: "success", url: response.download_url, file: response.file_name, }, })); }; const failDownload = (formatId: string | null, message: string) => { const key = formatId ?? DEFAULT_DOWNLOAD_KEY; setDownloads((prev) => ({ ...prev, [key]: { status: "error", message, }, })); }; const handleDownload = async (formatId: string | null) => { if (!url.trim()) { failDownload(formatId, "请先输入视频链接。"); return; } try { startDownload(formatId); const response = await fetch(`${API_BASE}/api/download`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ url: url.trim(), format_id: formatId ?? undefined, }), }); if (!response.ok) { const detail = await response.text(); throw new Error( detail || `下载失败,状态码 ${response.status.toString()}` ); } const payload = (await response.json()) as DownloadResponse; finishDownload(formatId, payload); } catch (error) { const message = error instanceof Error ? error.message : "下载失败,请稍后再试。"; failDownload(formatId, message); } }; return (
Video Fetcher

YTB视频下载服务

输入任意公共视频链接。系统将列出可用清晰度与格式,支持一键最佳质量下载或按需选择单独下载。

{infoState === "error" && infoError ? (

{infoError}

) : null}
{infoState === "loading" ? (
正在拉取视频信息,请稍候…
) : null} {videoInfo ? (
{videoInfo.thumbnail ? ( // eslint-disable-next-line @next/next/no-img-element {videoInfo.title} ) : null}

{videoInfo.title}

{videoInfo.uploader ? ( 发布者 · {videoInfo.uploader} ) : null} {formatDuration(videoInfo.duration) ? ( 时长 · {formatDuration(videoInfo.duration)} ) : null} 可用格式 · {sortedFormats.length}
一键最佳质量下载 自动采用服务推荐的最佳格式,适合快速下载。 {downloads[DEFAULT_DOWNLOAD_KEY]?.status === "success" ? ( {downloads[DEFAULT_DOWNLOAD_KEY].file} ) : null} {downloads[DEFAULT_DOWNLOAD_KEY]?.status === "error" ? ( {downloads[DEFAULT_DOWNLOAD_KEY].message} ) : null}

可选格式列表

支持逐项下载,适合精准控制清晰度或格式。
{sortedFormats.map((format) => { const key = format.format_id; const downloadState = downloads[key] ?? baseDownloadState; const videoCodec = format.vcodec && format.vcodec !== "none" ? format.vcodec.split(".")[0] : "无"; const audioCodec = format.acodec && format.acodec !== "none" ? format.acodec.split(".")[0] : "无"; const resolution = format.resolution ?? "未知"; const fps = format.fps && format.fps > 0 ? `${format.fps.toFixed( Number.isInteger(format.fps) ? 0 : 1 )} FPS` : null; return ( ); })}
格式 ID 分辨率 / FPS 编码 体积 操作
{format.format_id} {format.ext ? ( {format.ext} ) : null}
{resolution} {fps ? ( {fps} ) : null}
视频 · {videoCodec} 音频 · {audioCodec}
{formatFileSize(format.filesize ?? format.filesize_approx)}
{downloadState.status === "success" ? ( {downloadState.file} ) : null} {downloadState.status === "error" ? ( {downloadState.message} ) : null}
) : null}
); }