From cab19672e0136da9ef1d494ac40d76164479f7d3 Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Mon, 22 Dec 2025 21:57:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(API):=20=E6=B7=BB=E5=8A=A0=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E9=80=89=E9=9B=86=E4=BF=A1=E6=81=AF=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 /api/video/episodes 端点 - 支持解析B站视频链接(BV号和AV号格式) - 调用B站 API 获取视频分P信息 - 返回选集列表包含: 分P编号、标题、时长、cid等 - 对非B站视频返回空选集列表 --- src/app/api/video/episodes/route.ts | 149 ++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 src/app/api/video/episodes/route.ts diff --git a/src/app/api/video/episodes/route.ts b/src/app/api/video/episodes/route.ts new file mode 100644 index 0000000..6f4ddf5 --- /dev/null +++ b/src/app/api/video/episodes/route.ts @@ -0,0 +1,149 @@ +import { NextRequest, NextResponse } from 'next/server'; + +/** + * 视频选集信息 + */ +interface VideoEpisode { + page: number; // 分P编号,从1开始 + part: string; // 分P标题 + duration: number; // 时长(秒) + cid?: number; // B站视频分片ID +} + +/** + * B站 API 返回的视频详情结构 + */ +interface BilibiliVideoResponse { + code: number; + message: string; + data?: { + bvid: string; + title: string; + pages: { + cid: number; + page: number; + part: string; + duration: number; + }[]; + }; +} + +/** + * 从 URL 中解析 B站视频 ID + */ +function parseBilibiliUrl(url: string): { bvid?: string; aid?: string } | null { + try { + // BV号格式: bilibili.com/video/BVxxxxx + const bvMatch = url.match(/bilibili\.com\/video\/(BV[\w]+)/i); + if (bvMatch) { + return { bvid: bvMatch[1] }; + } + + // AV号格式: bilibili.com/video/avxxxxx + const avMatch = url.match(/bilibili\.com\/video\/av(\d+)/i); + if (avMatch) { + return { aid: avMatch[1] }; + } + + // 短链接格式: b23.tv/xxxxx (需要跟随重定向) + // 暂不支持,返回 null + + return null; + } catch { + return null; + } +} + +/** + * GET /api/video/episodes + * 获取视频选集列表 + * + * Query params: + * - url: 视频链接 + */ +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const videoUrl = searchParams.get('url'); + + if (!videoUrl) { + return NextResponse.json( + { error: '缺少 url 参数' }, + { status: 400 } + ); + } + + // 解析视频 URL + const videoId = parseBilibiliUrl(videoUrl); + + if (!videoId) { + // 不是 B站视频或无法解析,返回空选集 + return NextResponse.json({ + platform: 'unknown', + episodes: [], + totalEpisodes: 0, + }); + } + + // 构建 B站 API 请求 URL + let apiUrl = 'https://api.bilibili.com/x/web-interface/view?'; + if (videoId.bvid) { + apiUrl += `bvid=${videoId.bvid}`; + } else if (videoId.aid) { + apiUrl += `aid=${videoId.aid}`; + } + + // 调用 B站 API + const response = await fetch(apiUrl, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Referer': 'https://www.bilibili.com/', + }, + }); + + if (!response.ok) { + console.error('[API/video/episodes] B站 API 请求失败:', response.status); + return NextResponse.json({ + platform: 'bilibili', + episodes: [], + totalEpisodes: 0, + error: 'B站 API 请求失败', + }); + } + + const data: BilibiliVideoResponse = await response.json(); + + if (data.code !== 0 || !data.data) { + console.error('[API/video/episodes] B站 API 返回错误:', data.message); + return NextResponse.json({ + platform: 'bilibili', + episodes: [], + totalEpisodes: 0, + error: data.message || '获取视频信息失败', + }); + } + + // 提取选集信息 + const episodes: VideoEpisode[] = data.data.pages.map((page) => ({ + page: page.page, + part: page.part, + duration: page.duration, + cid: page.cid, + })); + + return NextResponse.json({ + platform: 'bilibili', + bvid: data.data.bvid, + title: data.data.title, + episodes, + totalEpisodes: episodes.length, + }); + + } catch (error) { + console.error('[API/video/episodes] 错误:', error); + return NextResponse.json( + { error: '获取选集信息失败' }, + { status: 500 } + ); + } +}