OrangeTV/src/app/api/admin/source/validate/route.ts

200 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';
import { API_CONFIG } from '@/lib/config';
export const runtime = 'nodejs';
export async function GET(request: NextRequest) {
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const searchKeyword = searchParams.get('q');
if (!searchKeyword) {
return new Response(
JSON.stringify({ error: '搜索关键词不能为空' }),
{
status: 400,
headers: {
'Content-Type': 'application/json',
},
}
);
}
const config = await getConfig();
const apiSites = config.SourceConfig;
// 共享状态
let streamClosed = false;
// 创建可读流
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
// 辅助函数:安全地向控制器写入数据
const safeEnqueue = (data: Uint8Array) => {
try {
if (streamClosed || (!controller.desiredSize && controller.desiredSize !== 0)) {
return false;
}
controller.enqueue(data);
return true;
} catch (error) {
console.warn('Failed to enqueue data:', error);
streamClosed = true;
return false;
}
};
// 发送开始事件
const startEvent = `data: ${JSON.stringify({
type: 'start',
totalSources: apiSites.length
})}\n\n`;
if (!safeEnqueue(encoder.encode(startEvent))) {
return;
}
// 记录已完成的源数量
let completedSources = 0;
// 为每个源创建验证 Promise
const validationPromises = apiSites.map(async (site) => {
try {
// 构建搜索URL只获取第一页
const searchUrl = `${site.api}?ac=videolist&wd=${encodeURIComponent(searchKeyword)}`;
// 设置超时控制
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(searchUrl, {
headers: API_CONFIG.search.headers,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json() as any;
// 检查结果是否有效
let status: 'valid' | 'no_results' | 'invalid';
if (
data &&
data.list &&
Array.isArray(data.list) &&
data.list.length > 0
) {
// 检查是否有标题包含搜索词的结果
const validResults = data.list.filter((item: any) => {
const title = item.vod_name || '';
return title.toLowerCase().includes(searchKeyword.toLowerCase());
});
if (validResults.length > 0) {
status = 'valid';
} else {
status = 'no_results';
}
} else {
status = 'no_results';
}
// 发送该源的验证结果
completedSources++;
if (!streamClosed) {
const sourceEvent = `data: ${JSON.stringify({
type: 'source_result',
source: site.key,
status
})}\n\n`;
if (!safeEnqueue(encoder.encode(sourceEvent))) {
streamClosed = true;
return;
}
}
} finally {
clearTimeout(timeoutId);
}
} catch (error) {
console.warn(`验证失败 ${site.name}:`, error);
// 发送源错误事件
completedSources++;
if (!streamClosed) {
const errorEvent = `data: ${JSON.stringify({
type: 'source_error',
source: site.key,
status: 'invalid'
})}\n\n`;
if (!safeEnqueue(encoder.encode(errorEvent))) {
streamClosed = true;
return;
}
}
}
// 检查是否所有源都已完成
if (completedSources === apiSites.length) {
if (!streamClosed) {
// 发送最终完成事件
const completeEvent = `data: ${JSON.stringify({
type: 'complete',
completedSources
})}\n\n`;
if (safeEnqueue(encoder.encode(completeEvent))) {
try {
controller.close();
} catch (error) {
console.warn('Failed to close controller:', error);
}
}
}
}
});
// 等待所有验证完成
await Promise.allSettled(validationPromises);
},
cancel() {
streamClosed = true;
console.log('Client disconnected, cancelling validation stream');
},
});
// 返回流式响应
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
}