openlist和emby异步搜索
This commit is contained in:
@@ -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 || '';
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user