feat: search api/page && detail api/page

This commit is contained in:
shinya
2025-06-18 00:59:11 +08:00
parent b38b513df0
commit 479fafcb1c
15 changed files with 1050 additions and 376 deletions

219
src/app/api/detail/route.ts Normal file
View File

@@ -0,0 +1,219 @@
import { NextResponse } from 'next/server';
import { API_CONFIG, ApiSite, getApiSites } from '@/lib/config';
const M3U8_PATTERN = /(https?:\/\/[^"'\s]+?\.m3u8)/g;
export interface VideoDetail {
code: number;
episodes: string[];
detailUrl: string;
videoInfo: {
title: string;
cover?: string;
desc?: string;
type?: string;
year?: string;
area?: string;
director?: string;
actor?: string;
remarks?: string;
source_name: string;
source: string;
id: string;
};
}
async function handleSpecialSourceDetail(
id: string,
apiSite: ApiSite
): Promise<VideoDetail> {
const detailUrl = `${apiSite.detail}/index.php/vod/detail/id/${id}.html`;
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 html = await response.text();
let matches: string[] = [];
if (apiSite.key === 'ffzy') {
const ffzyPattern =
/\$(https?:\/\/[^"'\s]+?\/\d{8}\/\d+_[a-f0-9]+\/index\.m3u8)/g;
matches = html.match(ffzyPattern) || [];
}
if (matches.length === 0) {
const generalPattern = /\$(https?:\/\/[^"'\s]+?\.m3u8)/g;
matches = html.match(generalPattern) || [];
}
matches = Array.from(new Set(matches));
matches = matches.map((link: string) => {
link = link.substring(1, link.length);
const parenIndex = link.indexOf('(');
return parenIndex > 0 ? link.substring(0, parenIndex) : link;
});
const titleMatch = html.match(/<h1[^>]*>([^<]+)<\/h1>/);
const titleText = titleMatch ? titleMatch[1].trim() : '';
const descMatch = html.match(
/<div[^>]*class=["']sketch["'][^>]*>([\s\S]*?)<\/div>/
);
const descText = descMatch
? descMatch[1].replace(/<[^>]+>/g, ' ').trim()
: '';
const coverMatch = html.match(/(https?:\/\/[^"'\s]+?\.jpg)/g);
const coverUrl = coverMatch ? coverMatch[0].trim() : '';
return {
code: 200,
episodes: matches,
detailUrl: detailUrl,
videoInfo: {
title: titleText,
cover: coverUrl,
desc: descText,
source_name: apiSite.name,
source: apiSite.key,
id: id,
},
};
}
async function getDetailFromApi(
apiSite: ApiSite,
id: string
): Promise<VideoDetail> {
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[] = [];
if (videoDetail.vod_play_url) {
const playSources = videoDetail.vod_play_url.split('$$$');
if (playSources.length > 0) {
const mainSource = playSources[0];
const episodeList = mainSource.split('#');
episodes = episodeList
.map((ep: string) => {
const parts = ep.split('$');
return parts.length > 1 ? parts[1] : '';
})
.filter(
(url: string) =>
url && (url.startsWith('http://') || url.startsWith('https://'))
);
}
}
if (episodes.length === 0 && videoDetail.vod_content) {
const matches = videoDetail.vod_content.match(M3U8_PATTERN) || [];
episodes = matches.map((link: string) => link.replace(/^\$/, ''));
}
return {
code: 200,
episodes: episodes,
detailUrl: detailUrl,
videoInfo: {
title: videoDetail.vod_name,
cover: videoDetail.vod_pic,
desc: videoDetail.vod_content,
type: videoDetail.type_name,
year: videoDetail.vod_year,
area: videoDetail.vod_area,
director: videoDetail.vod_director,
actor: videoDetail.vod_actor,
remarks: videoDetail.vod_remarks,
source_name: apiSite.name,
source: apiSite.key,
id: id,
},
};
}
export async function getVideoDetail(
id: string,
sourceCode: string
): Promise<VideoDetail> {
if (!id) {
throw new Error('缺少视频ID参数');
}
if (!/^[\w-]+$/.test(id)) {
throw new Error('无效的视频ID格式');
}
const apiSites = getApiSites();
const apiSite = apiSites.find((site) => site.key === sourceCode);
if (!apiSite) {
throw new Error('无效的API来源');
}
if (apiSite.detail) {
return await handleSpecialSourceDetail(id, apiSite);
} else {
return await getDetailFromApi(apiSite, id);
}
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
const sourceCode = searchParams.get('source');
if (!id || !sourceCode) {
return NextResponse.json({ error: '缺少必要参数' }, { status: 400 });
}
try {
const result = await getVideoDetail(id, sourceCode);
return NextResponse.json(result);
} catch (error) {
return NextResponse.json(
{ error: (error as Error).message },
{ status: 500 }
);
}
}

View File

@@ -1,5 +0,0 @@
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({ hello: 'Next.js' });
}

177
src/app/api/search/route.ts Normal file
View File

@@ -0,0 +1,177 @@
import { NextResponse } from 'next/server';
import { API_CONFIG, ApiSite, getApiSites } from '@/lib/config';
import { getVideoDetail } from '../detail/route';
interface SearchResult {
id: string;
title: string;
poster: string;
episodes?: number;
source: string;
source_name: string;
}
interface ApiSearchItem {
vod_id: string;
vod_name: string;
vod_pic: string;
vod_remarks?: string;
}
async function searchFromApi(
apiSite: ApiSite,
query: string
): Promise<SearchResult[]> {
try {
const apiBaseUrl = apiSite.api;
const apiUrl =
apiBaseUrl + API_CONFIG.search.path + encodeURIComponent(query);
const apiName = apiSite.name;
// 添加超时处理
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000);
const response = await fetch(apiUrl, {
headers: API_CONFIG.search.headers,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
return [];
}
const data = await response.json();
if (
!data ||
!data.list ||
!Array.isArray(data.list) ||
data.list.length === 0
) {
return [];
}
// 处理第一页结果
const results = data.list.map((item: ApiSearchItem) => ({
id: item.vod_id,
title: item.vod_name,
poster: item.vod_pic,
episodes: item.vod_remarks ? parseInt(item.vod_remarks) : undefined,
source: apiSite.key,
source_name: apiName,
}));
// 获取总页数
const pageCount = data.pagecount || 1;
// 确定需要获取的额外页数
const pagesToFetch = Math.min(
pageCount - 1,
API_CONFIG.search.maxPages - 1
);
// 如果有额外页数,获取更多页的结果
if (pagesToFetch > 0) {
const additionalPagePromises = [];
for (let page = 2; page <= pagesToFetch + 1; page++) {
const pageUrl =
apiBaseUrl +
API_CONFIG.search.pagePath
.replace('{query}', encodeURIComponent(query))
.replace('{page}', page.toString());
const pagePromise = (async () => {
try {
const pageController = new AbortController();
const pageTimeoutId = setTimeout(
() => pageController.abort(),
8000
);
const pageResponse = await fetch(pageUrl, {
headers: API_CONFIG.search.headers,
signal: pageController.signal,
});
clearTimeout(pageTimeoutId);
if (!pageResponse.ok) return [];
const pageData = await pageResponse.json();
if (!pageData || !pageData.list || !Array.isArray(pageData.list))
return [];
return pageData.list.map((item: ApiSearchItem) => ({
id: item.vod_id,
title: item.vod_name,
poster: item.vod_pic,
episodes: item.vod_remarks
? parseInt(item.vod_remarks)
: undefined,
source: apiSite.key,
source_name: apiName,
}));
} catch (error) {
return [];
}
})();
additionalPagePromises.push(pagePromise);
}
// 等待所有额外页的结果
const additionalResults = await Promise.all(additionalPagePromises);
// 合并所有页的结果
additionalResults.forEach((pageResults) => {
if (pageResults.length > 0) {
results.push(...pageResults);
}
});
}
// 获取每个结果的详情
const detailPromises = results.map(async (result: SearchResult) => {
try {
const detail = await getVideoDetail(result.id, result.source);
if (detail.episodes.length > 1) {
return { ...result, episodes: detail.episodes.length };
}
return result;
} catch (error) {
return result;
}
});
return await Promise.all(detailPromises);
} catch (error) {
return [];
}
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const query = searchParams.get('q');
if (!query) {
return NextResponse.json({ results: [] });
}
const apiSites = getApiSites();
const searchPromises = apiSites.map((site) => searchFromApi(site, query));
try {
const results = await Promise.all(searchPromises);
const flattenedResults = results.flat();
return NextResponse.json({ results: flattenedResults });
} catch (error) {
return NextResponse.json({ error: '搜索失败' }, { status: 500 });
}
}