获取视频源详情不再依赖title

This commit is contained in:
mtvpls
2026-03-18 09:51:23 +08:00
parent 826acf40bf
commit 02b5c87c01
3 changed files with 98 additions and 44 deletions

View File

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

View File

@@ -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

View File

@@ -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('获取视频详情失败');
}