Files
91/src/components/VideoMetaHeader.tsx
T
nianzhibai 7e5e67697e feat: add GuangYaPan drive support
Implement a new GuangYaPan cloud drive integration across the backend, admin UI, playback proxy, and Spider91 migration flow.

Backend changes:\n- Add a GuangYaPan drive driver with token refresh, QR/device login support, directory listing, stream link resolution, directory creation, rename/delete operations, OSS multipart upload, and upload task polling.\n- Register GuangYaPan as a supported storage kind in configuration, catalog normalization, admin APIs, public drive labels, and 302 playback redirects.\n- Allow Spider91 crawler uploads to target GuangYaPan through a dedicated migration adapter.\n- Add scan, thumbnail, preview, and fingerprint cooldown handling for GuangYaPan based on explicit HTTP status codes, Retry-After values, and structured provider codes instead of natural-language message matching.\n- Tighten existing provider cooldown detectors so OneDrive, Google Drive, 115, PikPak, 123pan, Wopan, and media workers avoid treating arbitrary response text as a rate-limit signal.\n- Keep large videos eligible for preview generation unless the user disables preview generation.

Admin and tooling changes:\n- Add GuangYaPan as a selectable drive type with QR login UI and token/root-path credential fields.\n- Add crawler upload target support for GuangYaPan in the admin UI.\n- Add drive branding, labels, metadata display, and docs/config examples for GuangYaPan.\n- Include a standalone GuangYaPan QR login helper script for manual credential acquisition.

Tests:\n- Add GuangYaPan driver, QR login, proxy, admin API, crawler upload target, fingerprint, cooldown, and form coverage.\n- Update rate-limit tests to assert that message-only throttling text no longer starts cooldowns.\n- Cover explicit HTTP status parsing through shared drive helper tests.
2026-06-14 15:44:50 +08:00

84 lines
2.9 KiB
TypeScript

import { CalendarDays, Clock3, Eye } from "lucide-react";
import type { VideoDetail } from "@/types";
import { formatCount } from "@/lib/format";
type Props = {
video: VideoDetail;
};
/**
* 详情页标题块。
*
* 视觉:
* - meta:一组小胶囊(来源、画质、时长、观看数、发布时间)
* 每个胶囊有自己的语义色彩,避免传统 "·" 分隔列表的列表感。
* - 标题:大、粗、最高两行,位于 meta 下方
*/
export function VideoMetaHeader({ video }: Props) {
const source = (video.sourceLabel ?? "").trim();
const quality = (video.quality ?? "").trim();
const duration = (video.duration ?? "").trim();
const published = (video.publishedAt ?? "").trim();
const sourceKind = sourceKindFromLabel(source);
return (
<header className="vd-header">
<div className="vd-header__row">
<ul className="vd-meta" aria-label="视频信息">
{source && (
<li className="vd-meta__chip" data-tone={sourceKind || "neutral"}>
<span className="vd-meta__dot" aria-hidden="true" />
{source}
</li>
)}
{quality && (
<li
className="vd-meta__chip"
data-tone={quality.toUpperCase() === "HD" ? "accent" : "neutral"}
>
{quality}
</li>
)}
{duration && (
<li className="vd-meta__chip vd-meta__chip--plain">
<Clock3 size={14} aria-hidden="true" />
{duration}
</li>
)}
<li className="vd-meta__chip vd-meta__chip--plain">
<Eye size={14} aria-hidden="true" />
<strong>{formatCount(video.views)}</strong>
</li>
{published && (
<li className="vd-meta__chip vd-meta__chip--plain">
<CalendarDays size={14} aria-hidden="true" />
{published}
</li>
)}
</ul>
</div>
<h1 className="vd-header__title" title={video.title}>
{video.title}
</h1>
</header>
);
}
// 根据 sourceLabel 识别网盘类型,用于胶囊配色。
function sourceKindFromLabel(label: string): string {
const value = label.toLowerCase();
if (value.includes("夸克") || value.includes("quark")) return "quark";
if (value.includes("115") || value.includes("p115")) return "p115";
if (value.includes("123") || value.includes("p123")) return "p123";
if (value.includes("pikpak")) return "pikpak";
if (value.includes("沃盘") || value.includes("wopan") || value.includes("联通"))
return "wopan";
if (value.includes("光鸭") || value.includes("guangyapan") || value.includes("guangya"))
return "guangyapan";
if (value.includes("onedrive") || value.includes("one drive")) return "onedrive";
if (value.includes("本地") || value.includes("localstorage") || value.includes("local storage"))
return "localstorage";
return "";
}