diff --git a/src/app/api/source-detail/route.ts b/src/app/api/source-detail/route.ts index 2c0af75..1616174 100644 --- a/src/app/api/source-detail/route.ts +++ b/src/app/api/source-detail/route.ts @@ -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, }; diff --git a/src/lib/downstream.ts b/src/lib/downstream.ts index 9652f61..ba85537 100644 --- a/src/lib/downstream.ts +++ b/src/lib/downstream.ts @@ -293,6 +293,90 @@ export async function getDetailFromApi( }; } +export async function getDetailFromApiV2( + apiSite: ApiSite, + id: string +): Promise { + 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 diff --git a/src/lib/fetchVideoDetail.ts b/src/lib/fetchVideoDetail.ts index 32168bf..6899afe 100644 --- a/src/lib/fetchVideoDetail.ts +++ b/src/lib/fetchVideoDetail.ts @@ -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 { // 检查是否是特殊源(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('获取视频详情失败'); }