获取视频源详情不再依赖title
This commit is contained in:
@@ -4,13 +4,13 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
||||
import { searchFromApi } from '@/lib/downstream';
|
||||
import { getDetailFromApiV2 } from '@/lib/downstream';
|
||||
import { getProxyToken } from '@/lib/emby-token';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
/**
|
||||
* 根据 source 和 id 从搜索结果中精确匹配获取视频详情
|
||||
* 根据 source 和 id 直接获取视频详情
|
||||
* 这个API专门用于play页面快速获取当前源的详情
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
@@ -22,10 +22,9 @@ export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
const sourceCode = searchParams.get('source');
|
||||
const title = searchParams.get('title'); // 用于搜索的标题
|
||||
const fileName = searchParams.get('fileName'); // 小雅源:用户点击的文件名
|
||||
|
||||
if (!id || !sourceCode || !title) {
|
||||
if (!id || !sourceCode) {
|
||||
return NextResponse.json({ error: '缺少必要参数' }, { status: 400 });
|
||||
}
|
||||
|
||||
@@ -385,7 +384,11 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// 对于其他源,通过搜索API获取,然后精确匹配
|
||||
if (!/^[\w-]+$/.test(id)) {
|
||||
return NextResponse.json({ error: '无效的视频ID格式' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 对于其他采集源,直接按 id 获取详情。
|
||||
try {
|
||||
const apiSites = await getAvailableApiSites(authInfo.username);
|
||||
const apiSite = apiSites.find((site) => site.key === sourceCode);
|
||||
@@ -394,26 +397,11 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: '无效的API来源' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 调用搜索API
|
||||
const searchResults = await searchFromApi(apiSite, title.trim());
|
||||
|
||||
// 从搜索结果中精确匹配 source 和 id
|
||||
const exactMatch = searchResults.find(
|
||||
(item: any) =>
|
||||
item.source?.toString() === sourceCode.toString() &&
|
||||
item.id?.toString() === id.toString()
|
||||
);
|
||||
|
||||
if (!exactMatch) {
|
||||
return NextResponse.json(
|
||||
{ error: '未找到匹配的视频源' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
const result = await getDetailFromApiV2(apiSite, id);
|
||||
|
||||
// 添加 proxyMode 到返回结果
|
||||
const resultWithProxy = {
|
||||
...exactMatch,
|
||||
...result,
|
||||
proxyMode: apiSite.proxyMode || false,
|
||||
};
|
||||
|
||||
|
||||
@@ -293,6 +293,90 @@ export async function getDetailFromApi(
|
||||
};
|
||||
}
|
||||
|
||||
export async function getDetailFromApiV2(
|
||||
apiSite: ApiSite,
|
||||
id: string
|
||||
): Promise<SearchResult> {
|
||||
const detailUrl = `${apiSite.api}${API_CONFIG.detail.path}${id}`;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
const response = await fetch(detailUrl, {
|
||||
headers: API_CONFIG.detail.headers,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`详情请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (
|
||||
!data ||
|
||||
!data.list ||
|
||||
!Array.isArray(data.list) ||
|
||||
data.list.length === 0
|
||||
) {
|
||||
throw new Error('获取到的详情内容无效');
|
||||
}
|
||||
|
||||
const videoDetail = data.list[0];
|
||||
let episodes: string[] = [];
|
||||
let titles: string[] = [];
|
||||
|
||||
if (videoDetail.vod_play_url) {
|
||||
const vodPlayUrlArray = videoDetail.vod_play_url.split('$$$');
|
||||
vodPlayUrlArray.forEach((url: string) => {
|
||||
const matchEpisodes: string[] = [];
|
||||
const matchTitles: string[] = [];
|
||||
const titleUrlArray = url.split('#');
|
||||
titleUrlArray.forEach((titleUrl: string) => {
|
||||
const episodeTitleUrl = titleUrl.split('$');
|
||||
if (
|
||||
episodeTitleUrl.length === 2 &&
|
||||
episodeTitleUrl[1].endsWith('.m3u8')
|
||||
) {
|
||||
matchTitles.push(episodeTitleUrl[0]);
|
||||
matchEpisodes.push(episodeTitleUrl[1]);
|
||||
}
|
||||
});
|
||||
if (matchEpisodes.length > episodes.length) {
|
||||
episodes = matchEpisodes;
|
||||
titles = matchTitles;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (episodes.length === 0 && videoDetail.vod_content) {
|
||||
const matches = videoDetail.vod_content.match(M3U8_PATTERN) || [];
|
||||
episodes = matches.map((link: string) => link.replace(/^\$/, ''));
|
||||
}
|
||||
|
||||
return {
|
||||
id: id.toString(),
|
||||
title: videoDetail.vod_name,
|
||||
poster: videoDetail.vod_pic,
|
||||
episodes,
|
||||
episodes_titles: titles,
|
||||
source: apiSite.key,
|
||||
source_name: apiSite.name,
|
||||
class: videoDetail.vod_class,
|
||||
year: videoDetail.vod_year
|
||||
? videoDetail.vod_year.match(/\d{4}/)?.[0] || ''
|
||||
: 'unknown',
|
||||
desc: cleanHtmlTags(videoDetail.vod_content),
|
||||
type_name: videoDetail.type_name,
|
||||
douban_id: videoDetail.vod_douban_id,
|
||||
vod_remarks: videoDetail.vod_remarks,
|
||||
vod_total: videoDetail.vod_total,
|
||||
proxyMode: apiSite.proxyMode || false,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleSpecialSourceDetail(
|
||||
id: string,
|
||||
apiSite: ApiSite
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getAvailableApiSites } from '@/lib/config';
|
||||
import { SearchResult } from '@/lib/types';
|
||||
|
||||
import { getDetailFromApi, searchFromApi } from './downstream';
|
||||
import { getDetailFromApiV2 } from './downstream';
|
||||
import { getSpecialSourceDetail, isSpecialSource } from './special-sources-detail';
|
||||
|
||||
interface FetchVideoDetailOptions {
|
||||
@@ -13,13 +13,12 @@ interface FetchVideoDetailOptions {
|
||||
/**
|
||||
* 根据 source 与 id 获取视频详情。
|
||||
* 1. 如果是特殊源(emby、openlist、xiaoya),直接调用对应的获取函数。
|
||||
* 2. 若传入 fallbackTitle,则先调用 /api/search 搜索精确匹配。
|
||||
* 3. 若搜索未命中或未提供 fallbackTitle,则直接调用 /api/detail。
|
||||
* 2. 其他采集源直接调用详情接口,避免依赖搜索接口。
|
||||
*/
|
||||
export async function fetchVideoDetail({
|
||||
source,
|
||||
id,
|
||||
fallbackTitle = '',
|
||||
fallbackTitle: _fallbackTitle = '',
|
||||
}: FetchVideoDetailOptions): Promise<SearchResult> {
|
||||
// 检查是否是特殊源(emby、openlist、xiaoya)
|
||||
if (isSpecialSource(source)) {
|
||||
@@ -30,30 +29,13 @@ export async function fetchVideoDetail({
|
||||
// 如果特殊源返回 null,继续使用标准流程
|
||||
}
|
||||
|
||||
// 优先通过搜索接口查找精确匹配
|
||||
const apiSites = await getAvailableApiSites();
|
||||
const apiSite = apiSites.find((site) => site.key === source);
|
||||
if (!apiSite) {
|
||||
throw new Error('无效的API来源');
|
||||
}
|
||||
if (fallbackTitle) {
|
||||
try {
|
||||
const searchData = await searchFromApi(apiSite, fallbackTitle.trim());
|
||||
const exactMatch = searchData.find(
|
||||
(item: SearchResult) =>
|
||||
item.source.toString() === source.toString() &&
|
||||
item.id.toString() === id.toString()
|
||||
);
|
||||
if (exactMatch) {
|
||||
return exactMatch;
|
||||
}
|
||||
} catch (error) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
// 调用 /api/detail 接口
|
||||
const detail = await getDetailFromApi(apiSite, id);
|
||||
const detail = await getDetailFromApiV2(apiSite, id);
|
||||
if (!detail) {
|
||||
throw new Error('获取视频详情失败');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user