diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts index 229c62c..2b5a0ca 100644 --- a/src/app/api/search/route.ts +++ b/src/app/api/search/route.ts @@ -36,6 +36,61 @@ export async function GET(request: NextRequest) { const config = await getConfig(); const apiSites = await getAvailableApiSites(authInfo.username); + // 检查是否配置了 OpenList + const hasOpenList = !!(config.OpenListConfig?.URL && config.OpenListConfig?.Username && config.OpenListConfig?.Password); + + // 搜索 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_titles: [], + year: info.release_date.split('-')[0] || '', + desc: info.overview, + type_name: info.media_type === 'movie' ? '电影' : '电视剧', + douban_id: 0, + })); + } + } catch (error) { + console.error('[Search] 搜索 OpenList 失败:', error); + } + } + // 添加超时控制和错误处理,避免慢接口拖累整体响应 const searchPromises = apiSites.map((site) => Promise.race([ @@ -54,7 +109,7 @@ export async function GET(request: NextRequest) { const successResults = results .filter((result) => result.status === 'fulfilled') .map((result) => (result as PromiseFulfilledResult).value); - let flattenedResults = successResults.flat(); + let flattenedResults = [...openlistResults, ...successResults.flat()]; if (!config.SiteConfig.DisableYellowFilter) { flattenedResults = flattenedResults.filter((result) => { const typeName = result.type_name || ''; diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index ea15678..4ba0b0f 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -606,9 +606,9 @@ function PlayPageClient() { // 工具函数(Utils) // ----------------------------------------------------------------------------- - // 判断剧集是否已完结 - const isSeriesCompleted = (detail: SearchResult | null): boolean => { - if (!detail) return false; + // 判断剧集状态 + const getSeriesStatus = (detail: SearchResult | null): 'completed' | 'ongoing' | 'unknown' => { + if (!detail) return 'unknown'; // 方法1:通过 vod_remarks 判断 if (detail.vod_remarks) { @@ -620,23 +620,27 @@ function PlayPageClient() { // 如果包含连载关键词,则为连载中 if (ongoingKeywords.some(keyword => remarks.includes(keyword))) { - return false; + return 'ongoing'; } // 如果包含完结关键词,则为已完结 if (completedKeywords.some(keyword => remarks.includes(keyword))) { - return true; + return 'completed'; } } // 方法2:通过 vod_total 和实际集数对比判断 if (detail.vod_total && detail.vod_total > 0 && detail.episodes && detail.episodes.length > 0) { // 如果实际集数 >= 总集数,则为已完结 - return detail.episodes.length >= detail.vod_total; + if (detail.episodes.length >= detail.vod_total) { + return 'completed'; + } + // 如果实际集数 < 总集数,则为连载中 + return 'ongoing'; } - // 无法判断,默认返回 false(连载中) - return false; + // 无法判断,返回 unknown + return 'unknown'; }; // 播放源优选函数 @@ -1776,7 +1780,9 @@ function PlayPageClient() { ? result.year.toLowerCase() === videoYearRef.current.toLowerCase() : true) && (searchType - ? (searchType === 'tv' && result.episodes.length > 1) || + ? // openlist 源跳过 episodes 长度检查,因为搜索时不返回详细播放列表 + result.source === 'openlist' || + (searchType === 'tv' && result.episodes.length > 1) || (searchType === 'movie' && result.episodes.length === 1) : true) ); @@ -1829,6 +1835,15 @@ function PlayPageClient() { ); if (target) { detailData = target; + + // 如果是 openlist 源且 episodes 为空,需要调用 detail 接口获取完整信息 + if (detailData.source === 'openlist' && (!detailData.episodes || detailData.episodes.length === 0)) { + console.log('[Play] OpenList source has no episodes, fetching detail...'); + const detailSources = await fetchSourceDetail(currentSource, currentId); + if (detailSources.length > 0) { + detailData = detailSources[0]; + } + } } else { setError('未找到匹配结果'); setLoading(false); @@ -1849,6 +1864,15 @@ function PlayPageClient() { console.log(detailData.source, detailData.id); + // 如果是 openlist 源且 episodes 为空,需要调用 detail 接口获取完整信息 + if (detailData.source === 'openlist' && (!detailData.episodes || detailData.episodes.length === 0)) { + console.log('[Play] OpenList source has no episodes after selection, fetching detail...'); + const detailSources = await fetchSourceDetail(detailData.source, detailData.id); + if (detailSources.length > 0) { + detailData = detailSources[0]; + } + } + setNeedPrefer(false); setCurrentSource(detailData.source); setCurrentId(detailData.id); @@ -2841,7 +2865,7 @@ function PlayPageClient() { total_episodes: detailRef.current?.episodes.length || 1, save_time: Date.now(), search_title: searchTitle, - is_completed: isSeriesCompleted(detailRef.current), + is_completed: getSeriesStatus(detailRef.current) === 'completed', vod_remarks: detailRef.current?.vod_remarks, }); setFavorited(true); @@ -4408,17 +4432,22 @@ function PlayPageClient() { )} {/* 完结状态标识 */} - {detail && totalEpisodes > 1 && ( - - {isSeriesCompleted(detail) ? '已完结' : '连载中'} - - )} + {detail && totalEpisodes > 1 && (() => { + const status = getSeriesStatus(detail); + if (status === 'unknown') return null; + + return ( + + {status === 'completed' ? '已完结' : '连载中'} + + ); + })()} {/* 第二行:播放器和选集 */}