feat(API): 添加视频选集信息获取接口
- 新增 /api/video/episodes 端点 - 支持解析B站视频链接(BV号和AV号格式) - 调用B站 API 获取视频分P信息 - 返回选集列表包含: 分P编号、标题、时长、cid等 - 对非B站视频返回空选集列表
This commit is contained in:
parent
1e81e9151b
commit
cab19672e0
149
src/app/api/video/episodes/route.ts
Normal file
149
src/app/api/video/episodes/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user