mirror of
https://github.com/nianzhibai/91.git
synced 2026-06-15 00:44:30 +08:00
7e5e67697e
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.
84 lines
2.9 KiB
TypeScript
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 "";
|
|
}
|