From 4484097f83343e8930fb5cf0a782828b711b662c Mon Sep 17 00:00:00 2001 From: mtvpls Date: Mon, 5 Jan 2026 14:24:16 +0800 Subject: [PATCH] =?UTF-8?q?openlist=E5=92=8Cemby=E5=BC=82=E6=AD=A5?= =?UTF-8?q?=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/search/route.ts | 175 ++++++++++++------------ src/app/api/search/ws/route.ts | 239 +++++++++++++++------------------ 2 files changed, 197 insertions(+), 217 deletions(-) diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts index c4330b4..43812cc 100644 --- a/src/app/api/search/route.ts +++ b/src/app/api/search/route.ts @@ -51,90 +51,94 @@ export async function GET(request: NextRequest) { config.EmbyConfig?.UserId ); - // 搜索 Emby(如果配置了) - let embyResults: any[] = []; - if (hasEmby) { - try { - const { EmbyClient } = await import('@/lib/emby.client'); - const client = new EmbyClient(config.EmbyConfig!); - - const searchResult = await client.getItems({ - searchTerm: query, - IncludeItemTypes: 'Movie,Series', - Recursive: true, - Fields: 'Overview,ProductionYear', - Limit: 50, - }); - - embyResults = searchResult.Items.map((item) => ({ - id: item.Id, - source: 'emby', - source_name: 'Emby', - title: item.Name, - poster: client.getImageUrl(item.Id, 'Primary'), - episodes: [], - episodes_titles: [], - year: item.ProductionYear?.toString() || '', - desc: item.Overview || '', - type_name: item.Type === 'Movie' ? '电影' : '电视剧', - douban_id: 0, - })); - } catch (error) { - console.error('[Search] 搜索 Emby 失败:', error); - } - } - - // 搜索 OpenList(如果配置了) - let openlistResults: any[] = []; - 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] 从数据库读取 metainfo 失败:', error); - } - } - - if (metaInfo && metaInfo.folders) { - 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), + // 搜索 Emby(如果配置了)- 异步带超时 + const embyPromise = hasEmby + ? Promise.race([ + (async () => { + const { EmbyClient } = await import('@/lib/emby.client'); + const client = new EmbyClient(config.EmbyConfig!); + const searchResult = await client.getItems({ + searchTerm: query, + IncludeItemTypes: 'Movie,Series', + Recursive: true, + Fields: 'Overview,ProductionYear', + Limit: 50, + }); + return searchResult.Items.map((item) => ({ + id: item.Id, + source: 'emby', + source_name: 'Emby', + title: item.Name, + poster: client.getImageUrl(item.Id, 'Primary'), episodes: [], episodes_titles: [], - year: info.release_date.split('-')[0] || '', - desc: info.overview, - type_name: info.media_type === 'movie' ? '电影' : '电视剧', + year: item.ProductionYear?.toString() || '', + desc: item.Overview || '', + type_name: item.Type === 'Movie' ? '电影' : '电视剧', douban_id: 0, })); - } - } catch (error) { - console.error('[Search] 搜索 OpenList 失败:', error); - } - } + })(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Emby timeout')), 20000) + ), + ]).catch((error) => { + console.error('[Search] 搜索 Emby 失败:', error); + return []; + }) + : Promise.resolve([]); + + // 搜索 OpenList(如果配置了)- 异步带超时 + const openlistPromise = hasOpenList + ? Promise.race([ + (async () => { + 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) { + const metainfoJson = await db.getGlobalValue('video.metainfo'); + if (metainfoJson) { + metaInfo = JSON.parse(metainfoJson); + if (metaInfo) { + setCachedMetaInfo(rootPath, metaInfo); + } + } + } + + if (metaInfo && metaInfo.folders) { + return 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, + })); + } + return []; + })(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('OpenList timeout')), 20000) + ), + ]).catch((error) => { + console.error('[Search] 搜索 OpenList 失败:', error); + return []; + }) + : Promise.resolve([]); // 添加超时控制和错误处理,避免慢接口拖累整体响应 const searchPromises = apiSites.map((site) => @@ -150,11 +154,12 @@ export async function GET(request: NextRequest) { ); try { - const results = await Promise.allSettled(searchPromises); - const successResults = results - .filter((result) => result.status === 'fulfilled') - .map((result) => (result as PromiseFulfilledResult).value); - let flattenedResults = [...embyResults, ...openlistResults, ...successResults.flat()]; + const [embyResults, openlistResults, ...apiResults] = await Promise.all([ + embyPromise, + openlistPromise, + ...searchPromises, + ]); + let flattenedResults = [...embyResults, ...openlistResults, ...apiResults.flat()]; if (!config.SiteConfig.DisableYellowFilter) { flattenedResults = flattenedResults.filter((result) => { const typeName = result.type_name || ''; diff --git a/src/app/api/search/ws/route.ts b/src/app/api/search/ws/route.ts index 8c6db20..353b2d9 100644 --- a/src/app/api/search/ws/route.ts +++ b/src/app/api/search/ws/route.ts @@ -89,88 +89,84 @@ export async function GET(request: NextRequest) { let completedSources = 0; const allResults: any[] = []; - // 搜索 Emby(如果配置了) + // 搜索 Emby(如果配置了)- 异步带超时 if (hasEmby) { - try { - const { EmbyClient } = await import('@/lib/emby.client'); - const client = new EmbyClient(config.EmbyConfig!); - - const searchResult = await client.getItems({ - searchTerm: query, - IncludeItemTypes: 'Movie,Series', - Recursive: true, - Fields: 'Overview,ProductionYear', - Limit: 50, + Promise.race([ + (async () => { + const { EmbyClient } = await import('@/lib/emby.client'); + const client = new EmbyClient(config.EmbyConfig!); + const searchResult = await client.getItems({ + searchTerm: query, + IncludeItemTypes: 'Movie,Series', + Recursive: true, + Fields: 'Overview,ProductionYear', + Limit: 50, + }); + return searchResult.Items.map((item) => ({ + id: item.Id, + source: 'emby', + source_name: 'Emby', + title: item.Name, + poster: client.getImageUrl(item.Id, 'Primary'), + episodes: [], + episodes_titles: [], + year: item.ProductionYear?.toString() || '', + desc: item.Overview || '', + type_name: item.Type === 'Movie' ? '电影' : '电视剧', + douban_id: 0, + })); + })(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Emby timeout')), 20000) + ), + ]) + .then((embyResults) => { + completedSources++; + if (!streamClosed) { + const sourceEvent = `data: ${JSON.stringify({ + type: 'source_result', + source: 'emby', + sourceName: 'Emby', + results: embyResults, + timestamp: Date.now() + })}\n\n`; + if (!safeEnqueue(encoder.encode(sourceEvent))) { + streamClosed = true; + return; + } + if (embyResults.length > 0) { + allResults.push(...embyResults); + } + } + }) + .catch((error) => { + console.error('[Search WS] 搜索 Emby 失败:', error); + completedSources++; + if (!streamClosed) { + const errorEvent = `data: ${JSON.stringify({ + type: 'source_error', + source: 'emby', + sourceName: 'Emby', + error: error instanceof Error ? error.message : '搜索失败', + timestamp: Date.now() + })}\n\n`; + safeEnqueue(encoder.encode(errorEvent)); + } }); - - const embyResults = searchResult.Items.map((item) => ({ - id: item.Id, - source: 'emby', - source_name: 'Emby', - title: item.Name, - poster: client.getImageUrl(item.Id, 'Primary'), - episodes: [], - episodes_titles: [], - year: item.ProductionYear?.toString() || '', - desc: item.Overview || '', - type_name: item.Type === 'Movie' ? '电影' : '电视剧', - douban_id: 0, - })); - - completedSources++; - - if (!streamClosed) { - const sourceEvent = `data: ${JSON.stringify({ - type: 'source_result', - source: 'emby', - sourceName: 'Emby', - results: embyResults, - timestamp: Date.now() - })}\n\n`; - - if (!safeEnqueue(encoder.encode(sourceEvent))) { - streamClosed = true; - return; - } - - if (embyResults.length > 0) { - allResults.push(...embyResults); - } - } - } catch (error) { - console.error('[Search WS] 搜索 Emby 失败:', error); - completedSources++; - - if (!streamClosed) { - const errorEvent = `data: ${JSON.stringify({ - type: 'source_error', - source: 'emby', - sourceName: 'Emby', - error: error instanceof Error ? error.message : '搜索失败', - timestamp: Date.now() - })}\n\n`; - - if (!safeEnqueue(encoder.encode(errorEvent))) { - streamClosed = true; - return; - } - } - } } - // 搜索 OpenList(如果配置了) + // 搜索 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'); + Promise.race([ + (async () => { + 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); + const rootPath = config.OpenListConfig!.RootPath || '/'; + let metaInfo = getCachedMetaInfo(rootPath); - // 如果没有缓存,尝试从数据库读取 - if (!metaInfo) { - try { + if (!metaInfo) { const metainfoJson = await db.getGlobalValue('video.metainfo'); if (metainfoJson) { metaInfo = JSON.parse(metainfoJson); @@ -178,34 +174,37 @@ export async function GET(request: NextRequest) { setCachedMetaInfo(rootPath, metaInfo); } } - } catch (error) { - console.error('[Search WS] 从数据库读取 metainfo 失败:', error); } - } - - if (metaInfo && metaInfo.folders) { - const openlistResults = Object.entries(metaInfo.folders) - .filter(([key, info]: [string, any]) => { - const matchFolder = info.folderName.toLowerCase().includes(query.toLowerCase()); - const matchTitle = info.title.toLowerCase().includes(query.toLowerCase()); - return matchFolder || matchTitle; - }) - .map(([key, info]: [string, any]) => ({ - id: key, - 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, - })); + if (metaInfo && metaInfo.folders) { + return Object.entries(metaInfo.folders) + .filter(([key, info]: [string, any]) => { + const matchFolder = info.folderName.toLowerCase().includes(query.toLowerCase()); + const matchTitle = info.title.toLowerCase().includes(query.toLowerCase()); + return matchFolder || matchTitle; + }) + .map(([key, info]: [string, any]) => ({ + id: key, + 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, + })); + } + return []; + })(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('OpenList timeout')), 20000) + ), + ]) + .then((openlistResults) => { completedSources++; - if (!streamClosed) { const sourceEvent = `data: ${JSON.stringify({ type: 'source_result', @@ -214,53 +213,29 @@ export async function GET(request: NextRequest) { results: openlistResults, timestamp: Date.now() })}\n\n`; - if (!safeEnqueue(encoder.encode(sourceEvent))) { streamClosed = true; return; } - if (openlistResults.length > 0) { allResults.push(...openlistResults); } } - } else { + }) + .catch((error) => { + console.error('[Search WS] 搜索 OpenList 失败:', error); completedSources++; - if (!streamClosed) { - const sourceEvent = `data: ${JSON.stringify({ - type: 'source_result', + const errorEvent = `data: ${JSON.stringify({ + type: 'source_error', source: 'openlist', sourceName: '私人影库', - results: [], + error: error instanceof Error ? error.message : '搜索失败', timestamp: Date.now() })}\n\n`; - - if (!safeEnqueue(encoder.encode(sourceEvent))) { - streamClosed = true; - return; - } + safeEnqueue(encoder.encode(errorEvent)); } - } - } 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