获取视频源详情不再依赖title
This commit is contained in:
@@ -4,13 +4,13 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
||||||
import { searchFromApi } from '@/lib/downstream';
|
import { getDetailFromApiV2 } from '@/lib/downstream';
|
||||||
import { getProxyToken } from '@/lib/emby-token';
|
import { getProxyToken } from '@/lib/emby-token';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据 source 和 id 从搜索结果中精确匹配获取视频详情
|
* 根据 source 和 id 直接获取视频详情
|
||||||
* 这个API专门用于play页面快速获取当前源的详情
|
* 这个API专门用于play页面快速获取当前源的详情
|
||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
@@ -22,10 +22,9 @@ export async function GET(request: NextRequest) {
|
|||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const id = searchParams.get('id');
|
const id = searchParams.get('id');
|
||||||
const sourceCode = searchParams.get('source');
|
const sourceCode = searchParams.get('source');
|
||||||
const title = searchParams.get('title'); // 用于搜索的标题
|
|
||||||
const fileName = searchParams.get('fileName'); // 小雅源:用户点击的文件名
|
const fileName = searchParams.get('fileName'); // 小雅源:用户点击的文件名
|
||||||
|
|
||||||
if (!id || !sourceCode || !title) {
|
if (!id || !sourceCode) {
|
||||||
return NextResponse.json({ error: '缺少必要参数' }, { status: 400 });
|
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 {
|
try {
|
||||||
const apiSites = await getAvailableApiSites(authInfo.username);
|
const apiSites = await getAvailableApiSites(authInfo.username);
|
||||||
const apiSite = apiSites.find((site) => site.key === sourceCode);
|
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 });
|
return NextResponse.json({ error: '无效的API来源' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用搜索API
|
const result = await getDetailFromApiV2(apiSite, id);
|
||||||
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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加 proxyMode 到返回结果
|
// 添加 proxyMode 到返回结果
|
||||||
const resultWithProxy = {
|
const resultWithProxy = {
|
||||||
...exactMatch,
|
...result,
|
||||||
proxyMode: apiSite.proxyMode || false,
|
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(
|
async function handleSpecialSourceDetail(
|
||||||
id: string,
|
id: string,
|
||||||
apiSite: ApiSite
|
apiSite: ApiSite
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { getAvailableApiSites } from '@/lib/config';
|
import { getAvailableApiSites } from '@/lib/config';
|
||||||
import { SearchResult } from '@/lib/types';
|
import { SearchResult } from '@/lib/types';
|
||||||
|
|
||||||
import { getDetailFromApi, searchFromApi } from './downstream';
|
import { getDetailFromApiV2 } from './downstream';
|
||||||
import { getSpecialSourceDetail, isSpecialSource } from './special-sources-detail';
|
import { getSpecialSourceDetail, isSpecialSource } from './special-sources-detail';
|
||||||
|
|
||||||
interface FetchVideoDetailOptions {
|
interface FetchVideoDetailOptions {
|
||||||
@@ -13,13 +13,12 @@ interface FetchVideoDetailOptions {
|
|||||||
/**
|
/**
|
||||||
* 根据 source 与 id 获取视频详情。
|
* 根据 source 与 id 获取视频详情。
|
||||||
* 1. 如果是特殊源(emby、openlist、xiaoya),直接调用对应的获取函数。
|
* 1. 如果是特殊源(emby、openlist、xiaoya),直接调用对应的获取函数。
|
||||||
* 2. 若传入 fallbackTitle,则先调用 /api/search 搜索精确匹配。
|
* 2. 其他采集源直接调用详情接口,避免依赖搜索接口。
|
||||||
* 3. 若搜索未命中或未提供 fallbackTitle,则直接调用 /api/detail。
|
|
||||||
*/
|
*/
|
||||||
export async function fetchVideoDetail({
|
export async function fetchVideoDetail({
|
||||||
source,
|
source,
|
||||||
id,
|
id,
|
||||||
fallbackTitle = '',
|
fallbackTitle: _fallbackTitle = '',
|
||||||
}: FetchVideoDetailOptions): Promise<SearchResult> {
|
}: FetchVideoDetailOptions): Promise<SearchResult> {
|
||||||
// 检查是否是特殊源(emby、openlist、xiaoya)
|
// 检查是否是特殊源(emby、openlist、xiaoya)
|
||||||
if (isSpecialSource(source)) {
|
if (isSpecialSource(source)) {
|
||||||
@@ -30,30 +29,13 @@ export async function fetchVideoDetail({
|
|||||||
// 如果特殊源返回 null,继续使用标准流程
|
// 如果特殊源返回 null,继续使用标准流程
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优先通过搜索接口查找精确匹配
|
|
||||||
const apiSites = await getAvailableApiSites();
|
const apiSites = await getAvailableApiSites();
|
||||||
const apiSite = apiSites.find((site) => site.key === source);
|
const apiSite = apiSites.find((site) => site.key === source);
|
||||||
if (!apiSite) {
|
if (!apiSite) {
|
||||||
throw new Error('无效的API来源');
|
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 getDetailFromApiV2(apiSite, id);
|
||||||
const detail = await getDetailFromApi(apiSite, id);
|
|
||||||
if (!detail) {
|
if (!detail) {
|
||||||
throw new Error('获取视频详情失败');
|
throw new Error('获取视频详情失败');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user