openlist和emby异步搜索

This commit is contained in:
mtvpls
2026-01-05 14:24:16 +08:00
parent 1f2dcaa08c
commit 4484097f83
2 changed files with 197 additions and 217 deletions

View File

@@ -51,90 +51,94 @@ export async function GET(request: NextRequest) {
config.EmbyConfig?.UserId config.EmbyConfig?.UserId
); );
// 搜索 Emby如果配置了 // 搜索 Emby如果配置了- 异步带超时
let embyResults: any[] = []; const embyPromise = hasEmby
if (hasEmby) { ? Promise.race([
try { (async () => {
const { EmbyClient } = await import('@/lib/emby.client'); const { EmbyClient } = await import('@/lib/emby.client');
const client = new EmbyClient(config.EmbyConfig!); const client = new EmbyClient(config.EmbyConfig!);
const searchResult = await client.getItems({
const searchResult = await client.getItems({ searchTerm: query,
searchTerm: query, IncludeItemTypes: 'Movie,Series',
IncludeItemTypes: 'Movie,Series', Recursive: true,
Recursive: true, Fields: 'Overview,ProductionYear',
Fields: 'Overview,ProductionYear', Limit: 50,
Limit: 50, });
}); return searchResult.Items.map((item) => ({
id: item.Id,
embyResults = searchResult.Items.map((item) => ({ source: 'emby',
id: item.Id, source_name: 'Emby',
source: 'emby', title: item.Name,
source_name: 'Emby', poster: client.getImageUrl(item.Id, 'Primary'),
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),
episodes: [], episodes: [],
episodes_titles: [], episodes_titles: [],
year: info.release_date.split('-')[0] || '', year: item.ProductionYear?.toString() || '',
desc: info.overview, desc: item.Overview || '',
type_name: info.media_type === 'movie' ? '电影' : '电视剧', type_name: item.Type === 'Movie' ? '电影' : '电视剧',
douban_id: 0, douban_id: 0,
})); }));
} })(),
} catch (error) { new Promise<any[]>((_, reject) =>
console.error('[Search] 搜索 OpenList 失败:', error); 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<any[]>((_, reject) =>
setTimeout(() => reject(new Error('OpenList timeout')), 20000)
),
]).catch((error) => {
console.error('[Search] 搜索 OpenList 失败:', error);
return [];
})
: Promise.resolve([]);
// 添加超时控制和错误处理,避免慢接口拖累整体响应 // 添加超时控制和错误处理,避免慢接口拖累整体响应
const searchPromises = apiSites.map((site) => const searchPromises = apiSites.map((site) =>
@@ -150,11 +154,12 @@ export async function GET(request: NextRequest) {
); );
try { try {
const results = await Promise.allSettled(searchPromises); const [embyResults, openlistResults, ...apiResults] = await Promise.all([
const successResults = results embyPromise,
.filter((result) => result.status === 'fulfilled') openlistPromise,
.map((result) => (result as PromiseFulfilledResult<any>).value); ...searchPromises,
let flattenedResults = [...embyResults, ...openlistResults, ...successResults.flat()]; ]);
let flattenedResults = [...embyResults, ...openlistResults, ...apiResults.flat()];
if (!config.SiteConfig.DisableYellowFilter) { if (!config.SiteConfig.DisableYellowFilter) {
flattenedResults = flattenedResults.filter((result) => { flattenedResults = flattenedResults.filter((result) => {
const typeName = result.type_name || ''; const typeName = result.type_name || '';

View File

@@ -89,88 +89,84 @@ export async function GET(request: NextRequest) {
let completedSources = 0; let completedSources = 0;
const allResults: any[] = []; const allResults: any[] = [];
// 搜索 Emby如果配置了 // 搜索 Emby如果配置了- 异步带超时
if (hasEmby) { if (hasEmby) {
try { Promise.race([
const { EmbyClient } = await import('@/lib/emby.client'); (async () => {
const client = new EmbyClient(config.EmbyConfig!); const { EmbyClient } = await import('@/lib/emby.client');
const client = new EmbyClient(config.EmbyConfig!);
const searchResult = await client.getItems({ const searchResult = await client.getItems({
searchTerm: query, searchTerm: query,
IncludeItemTypes: 'Movie,Series', IncludeItemTypes: 'Movie,Series',
Recursive: true, Recursive: true,
Fields: 'Overview,ProductionYear', Fields: 'Overview,ProductionYear',
Limit: 50, 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) { if (hasOpenList) {
try { Promise.race([
const { getCachedMetaInfo, setCachedMetaInfo } = await import('@/lib/openlist-cache'); (async () => {
const { getTMDBImageUrl } = await import('@/lib/tmdb.search'); const { getCachedMetaInfo, setCachedMetaInfo } = await import('@/lib/openlist-cache');
const { db } = await import('@/lib/db'); const { getTMDBImageUrl } = await import('@/lib/tmdb.search');
const { db } = await import('@/lib/db');
const rootPath = config.OpenListConfig!.RootPath || '/'; const rootPath = config.OpenListConfig!.RootPath || '/';
let metaInfo = getCachedMetaInfo(rootPath); let metaInfo = getCachedMetaInfo(rootPath);
// 如果没有缓存,尝试从数据库读取 if (!metaInfo) {
if (!metaInfo) {
try {
const metainfoJson = await db.getGlobalValue('video.metainfo'); const metainfoJson = await db.getGlobalValue('video.metainfo');
if (metainfoJson) { if (metainfoJson) {
metaInfo = JSON.parse(metainfoJson); metaInfo = JSON.parse(metainfoJson);
@@ -178,34 +174,37 @@ export async function GET(request: NextRequest) {
setCachedMetaInfo(rootPath, metaInfo); 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++; completedSources++;
if (!streamClosed) { if (!streamClosed) {
const sourceEvent = `data: ${JSON.stringify({ const sourceEvent = `data: ${JSON.stringify({
type: 'source_result', type: 'source_result',
@@ -214,53 +213,29 @@ export async function GET(request: NextRequest) {
results: openlistResults, results: openlistResults,
timestamp: Date.now() timestamp: Date.now()
})}\n\n`; })}\n\n`;
if (!safeEnqueue(encoder.encode(sourceEvent))) { if (!safeEnqueue(encoder.encode(sourceEvent))) {
streamClosed = true; streamClosed = true;
return; return;
} }
if (openlistResults.length > 0) { if (openlistResults.length > 0) {
allResults.push(...openlistResults); allResults.push(...openlistResults);
} }
} }
} else { })
.catch((error) => {
console.error('[Search WS] 搜索 OpenList 失败:', error);
completedSources++; completedSources++;
if (!streamClosed) { if (!streamClosed) {
const sourceEvent = `data: ${JSON.stringify({ const errorEvent = `data: ${JSON.stringify({
type: 'source_result', type: 'source_error',
source: 'openlist', source: 'openlist',
sourceName: '私人影库', sourceName: '私人影库',
results: [], error: error instanceof Error ? error.message : '搜索失败',
timestamp: Date.now() timestamp: Date.now()
})}\n\n`; })}\n\n`;
safeEnqueue(encoder.encode(errorEvent));
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 // 为每个源创建搜索 Promise