feat(API): 添加视频选集信息获取接口

- 新增 /api/video/episodes 端点
- 支持解析B站视频链接(BV号和AV号格式)
- 调用B站 API 获取视频分P信息
- 返回选集列表包含: 分P编号、标题、时长、cid等
- 对非B站视频返回空选集列表
This commit is contained in:
gaoziman 2025-12-22 21:57:25 +08:00
parent 1e81e9151b
commit cab19672e0

View File

@ -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 }
);
}
}