mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-17 01:33:43 +08:00
236 lines
7.0 KiB
TypeScript
236 lines
7.0 KiB
TypeScript
/**
|
|
* Search API Route
|
|
* Handles video search requests and aggregates results from multiple sources
|
|
* Now with automatic source availability detection
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { searchVideos } from '@/lib/api/client';
|
|
import { getEnabledSources, getSourceById } from '@/lib/api/video-sources';
|
|
import { checkMultipleVideos } from '@/lib/utils/source-checker';
|
|
import type { SearchRequest, SearchResult } from '@/lib/types';
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const body: SearchRequest = await request.json();
|
|
const { query, sources: sourceIds, page = 1 } = body;
|
|
|
|
// Validate input
|
|
if (!query || typeof query !== 'string' || query.trim().length === 0) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid or missing query parameter' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
if (!sourceIds || !Array.isArray(sourceIds) || sourceIds.length === 0) {
|
|
return NextResponse.json(
|
|
{ error: 'At least one source must be specified' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Get source configurations
|
|
const sources = sourceIds
|
|
.map((id: string) => getSourceById(id))
|
|
.filter((source): source is NonNullable<typeof source> => source !== undefined);
|
|
|
|
if (sources.length === 0) {
|
|
return NextResponse.json(
|
|
{ error: 'No valid sources found' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Perform parallel search across sources
|
|
const searchResults = await searchVideos(query.trim(), sources, page);
|
|
|
|
// Get source name mapping
|
|
const getSourceName = (sourceId: string): string => {
|
|
const sourceNames: Record<string, string> = {
|
|
'dytt': '电影天堂',
|
|
'ruyi': '如意',
|
|
'baofeng': '暴风',
|
|
'tianya': '天涯',
|
|
'feifan': '非凡影视',
|
|
'sanliuling': '360',
|
|
'wolong': '卧龙',
|
|
'jisu': '极速',
|
|
'mozhua': '魔爪',
|
|
'modu': '魔都',
|
|
'zuida': '最大',
|
|
'yinghua': '樱花',
|
|
'baiduyun': '百度云',
|
|
'wujin': '无尽',
|
|
'wangwang': '旺旺',
|
|
'ikun': 'iKun',
|
|
};
|
|
return sourceNames[sourceId] || sourceId;
|
|
};
|
|
|
|
// Get all videos from all sources
|
|
const allVideos = searchResults.flatMap(r => r.results);
|
|
|
|
// Check each video individually with improved accuracy (reduced concurrency)
|
|
const availableVideos = await checkMultipleVideos(allVideos, 8);
|
|
|
|
// Group available videos by source
|
|
const videosBySource = new Map<string, any[]>();
|
|
for (const video of availableVideos) {
|
|
const sourceId = video.source;
|
|
if (!videosBySource.has(sourceId)) {
|
|
videosBySource.set(sourceId, []);
|
|
}
|
|
videosBySource.get(sourceId)!.push(video);
|
|
}
|
|
|
|
// Build response with actual video counts per source
|
|
const response: SearchResult[] = Array.from(videosBySource.entries()).map(([sourceId, videos]) => ({
|
|
results: videos,
|
|
source: sourceId,
|
|
responseTime: searchResults.find(sr => sr.source === sourceId)?.responseTime,
|
|
}));
|
|
|
|
// Calculate source statistics
|
|
const sourceStats = sourceIds.map(sourceId => {
|
|
const count = videosBySource.get(sourceId)?.length || 0;
|
|
return {
|
|
sourceId,
|
|
sourceName: getSourceName(sourceId),
|
|
count,
|
|
};
|
|
});
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
query: query.trim(),
|
|
page,
|
|
sources: response,
|
|
totalResults: availableVideos.length,
|
|
sourceStats, // Include real counts per source
|
|
});
|
|
} catch (error) {
|
|
console.error('Search API error:', error);
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Internal server error',
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// Support GET method for simple queries
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const searchParams = request.nextUrl.searchParams;
|
|
const query = searchParams.get('q') || searchParams.get('query');
|
|
const sourcesParam = searchParams.get('sources');
|
|
const page = parseInt(searchParams.get('page') || '1', 10);
|
|
|
|
if (!query) {
|
|
return NextResponse.json(
|
|
{ error: 'Missing query parameter' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Use all enabled sources if not specified
|
|
const sourceIds = sourcesParam
|
|
? sourcesParam.split(',')
|
|
: getEnabledSources().map(s => s.id);
|
|
|
|
// Get source configurations
|
|
const sources = sourceIds
|
|
.map((id: string) => getSourceById(id))
|
|
.filter((source): source is NonNullable<typeof source> => source !== undefined);
|
|
|
|
if (sources.length === 0) {
|
|
return NextResponse.json(
|
|
{ error: 'No valid sources found' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Perform search
|
|
const searchResults = await searchVideos(query.trim(), sources, page);
|
|
|
|
// Get source name mapping
|
|
const getSourceName = (sourceId: string): string => {
|
|
const sourceNames: Record<string, string> = {
|
|
'dytt': '电影天堂',
|
|
'ruyi': '如意',
|
|
'baofeng': '暴风',
|
|
'tianya': '天涯',
|
|
'feifan': '非凡影视',
|
|
'sanliuling': '360',
|
|
'wolong': '卧龙',
|
|
'jisu': '极速',
|
|
'mozhua': '魔爪',
|
|
'modu': '魔都',
|
|
'zuida': '最大',
|
|
'yinghua': '樱花',
|
|
'baiduyun': '百度云',
|
|
'wujin': '无尽',
|
|
'wangwang': '旺旺',
|
|
'ikun': 'iKun',
|
|
};
|
|
return sourceNames[sourceId] || sourceId;
|
|
};
|
|
|
|
// Get all videos from all sources
|
|
const allVideos = searchResults.flatMap(r => r.results);
|
|
|
|
// Check each video individually with improved accuracy (reduced concurrency)
|
|
const availableVideos = await checkMultipleVideos(allVideos, 8);
|
|
|
|
// Group available videos by source
|
|
const videosBySource = new Map<string, any[]>();
|
|
for (const video of availableVideos) {
|
|
const sourceId = video.source;
|
|
if (!videosBySource.has(sourceId)) {
|
|
videosBySource.set(sourceId, []);
|
|
}
|
|
videosBySource.get(sourceId)!.push(video);
|
|
}
|
|
|
|
// Build response with actual video counts per source
|
|
const response: SearchResult[] = Array.from(videosBySource.entries()).map(([sourceId, videos]) => ({
|
|
results: videos,
|
|
source: sourceId,
|
|
responseTime: searchResults.find(sr => sr.source === sourceId)?.responseTime,
|
|
}));
|
|
|
|
// Calculate source statistics
|
|
const sourceStats = sourceIds.map(sourceId => {
|
|
const count = videosBySource.get(sourceId)?.length || 0;
|
|
return {
|
|
sourceId,
|
|
sourceName: getSourceName(sourceId),
|
|
count,
|
|
};
|
|
});
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
query: query.trim(),
|
|
page,
|
|
sources: response,
|
|
totalResults: availableVideos.length,
|
|
sourceStats, // Include real counts per source
|
|
});
|
|
} catch (error) {
|
|
console.error('Search API error:', error);
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Internal server error',
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|