Files
MoonTVPlus/src/app/api/search/ws/route.ts
T

297 lines
9.3 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getAvailableApiSites, getConfig } from '@/lib/config';
import { searchFromApi } from '@/lib/downstream';
import { yellowWords } from '@/lib/yellow';
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 query = searchParams.get('q');
if (!query) {
return new Response(
JSON.stringify({ error: '搜索关键词不能为空' }),
{
status: 400,
headers: {
'Content-Type': 'application/json',
},
}
);
}
const config = await getConfig();
const apiSites = await getAvailableApiSites(authInfo.username);
// 检查是否配置了 OpenList
const hasOpenList = !!(config.OpenListConfig?.URL && config.OpenListConfig?.Token);
// 共享状态
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',
query,
totalSources: apiSites.length + (hasOpenList ? 1 : 0),
timestamp: Date.now()
})}\n\n`;
if (!safeEnqueue(encoder.encode(startEvent))) {
return; // 连接已关闭,提前退出
}
// 记录已完成的源数量
let completedSources = 0;
const allResults: any[] = [];
// 搜索 OpenList(如果配置了)
if (hasOpenList) {
try {
const { getCachedMetaInfo, setCachedMetaInfo } = await import('@/lib/openlist-cache');
const { getTMDBImageUrl } = await import('@/lib/tmdb.search');
const { db } = await import('@/lib/db');
const rootPath = config.OpenListConfig!.RootPath || '/';
let metaInfo = getCachedMetaInfo(rootPath);
// 如果没有缓存,尝试从数据库读取
if (!metaInfo) {
try {
const metainfoJson = await db.getGlobalValue('video.metainfo');
if (metainfoJson) {
metaInfo = JSON.parse(metainfoJson);
if (metaInfo) {
setCachedMetaInfo(rootPath, metaInfo);
}
}
} catch (error) {
console.error('[Search WS] 从数据库读取 metainfo 失败:', error);
}
}
if (metaInfo && metaInfo.folders) {
const openlistResults = Object.entries(metaInfo.folders)
.filter(([folderName, info]: [string, any]) => {
const matchFolder = folderName.toLowerCase().includes(query.toLowerCase());
const matchTitle = info.title.toLowerCase().includes(query.toLowerCase());
return matchFolder || matchTitle;
})
.map(([folderName, info]: [string, any]) => ({
id: folderName,
source: 'openlist',
source_name: '私人影库',
title: info.title,
poster: getTMDBImageUrl(info.poster_path),
episodes: [],
episodes_titles: [],
year: info.release_date.split('-')[0] || '',
desc: info.overview,
type_name: info.media_type === 'movie' ? '电影' : '电视剧',
douban_id: 0,
}));
completedSources++;
if (!streamClosed) {
const sourceEvent = `data: ${JSON.stringify({
type: 'source_result',
source: 'openlist',
sourceName: '私人影库',
results: openlistResults,
timestamp: Date.now()
})}\n\n`;
if (!safeEnqueue(encoder.encode(sourceEvent))) {
streamClosed = true;
return;
}
if (openlistResults.length > 0) {
allResults.push(...openlistResults);
}
}
} else {
completedSources++;
if (!streamClosed) {
const sourceEvent = `data: ${JSON.stringify({
type: 'source_result',
source: 'openlist',
sourceName: '私人影库',
results: [],
timestamp: Date.now()
})}\n\n`;
if (!safeEnqueue(encoder.encode(sourceEvent))) {
streamClosed = true;
return;
}
}
}
} catch (error) {
console.error('[Search WS] 搜索 OpenList 失败:', error);
completedSources++;
if (!streamClosed) {
const errorEvent = `data: ${JSON.stringify({
type: 'source_error',
source: 'openlist',
sourceName: '私人影库',
error: error instanceof Error ? error.message : '搜索失败',
timestamp: Date.now()
})}\n\n`;
if (!safeEnqueue(encoder.encode(errorEvent))) {
streamClosed = true;
return;
}
}
}
}
// 为每个源创建搜索 Promise
const searchPromises = apiSites.map(async (site) => {
try {
// 添加超时控制
const searchPromise = Promise.race([
searchFromApi(site, query),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`${site.name} timeout`)), 20000)
),
]);
const results = await searchPromise as any[];
// 过滤黄色内容
let filteredResults = results;
if (!config.SiteConfig.DisableYellowFilter) {
filteredResults = results.filter((result) => {
const typeName = result.type_name || '';
return !yellowWords.some((word: string) => typeName.includes(word));
});
}
// 发送该源的搜索结果
completedSources++;
if (!streamClosed) {
const sourceEvent = `data: ${JSON.stringify({
type: 'source_result',
source: site.key,
sourceName: site.name,
results: filteredResults,
timestamp: Date.now()
})}\n\n`;
if (!safeEnqueue(encoder.encode(sourceEvent))) {
streamClosed = true;
return; // 连接已关闭,停止处理
}
}
if (filteredResults.length > 0) {
allResults.push(...filteredResults);
}
} catch (error) {
console.warn(`搜索失败 ${site.name}:`, error);
// 发送源错误事件
completedSources++;
if (!streamClosed) {
const errorEvent = `data: ${JSON.stringify({
type: 'source_error',
source: site.key,
sourceName: site.name,
error: error instanceof Error ? error.message : '搜索失败',
timestamp: Date.now()
})}\n\n`;
if (!safeEnqueue(encoder.encode(errorEvent))) {
streamClosed = true;
return; // 连接已关闭,停止处理
}
}
}
// 检查是否所有源都已完成
if (completedSources === apiSites.length + (hasOpenList ? 1 : 0)) {
if (!streamClosed) {
// 发送最终完成事件
const completeEvent = `data: ${JSON.stringify({
type: 'complete',
totalResults: allResults.length,
completedSources,
timestamp: Date.now()
})}\n\n`;
if (safeEnqueue(encoder.encode(completeEvent))) {
// 只有在成功发送完成事件后才关闭流
try {
controller.close();
} catch (error) {
console.warn('Failed to close controller:', error);
}
}
}
}
});
// 等待所有搜索完成
await Promise.allSettled(searchPromises);
},
cancel() {
// 客户端断开连接时,标记流已关闭
streamClosed = true;
console.log('Client disconnected, cancelling search 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',
},
});
}