From b64ce1c3f2c7c9b750dd58945c2d3858a8c18df6 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Wed, 24 Dec 2025 09:47:46 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E7=A7=81=E4=BA=BA=E5=BD=B1=E5=BA=93?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E4=B8=8D=E5=86=8D=E4=BB=8Ehttp=E5=86=85?= =?UTF-8?q?=E9=83=A8=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/detail/route.ts | 102 +++++++++++++++++++++++++++--------- 1 file changed, 78 insertions(+), 24 deletions(-) diff --git a/src/app/api/detail/route.ts b/src/app/api/detail/route.ts index 580cd4a..8a03620 100644 --- a/src/app/api/detail/route.ts +++ b/src/app/api/detail/route.ts @@ -31,12 +31,12 @@ export async function GET(request: NextRequest) { } const rootPath = openListConfig.RootPath || '/'; + const folderPath = `${rootPath}${rootPath.endsWith('/') ? '' : '/'}${id}`; // 1. 读取 metainfo 获取元数据 let metaInfo: any = null; try { const { getCachedMetaInfo, setCachedMetaInfo } = await import('@/lib/openlist-cache'); - const { getTMDBImageUrl } = await import('@/lib/tmdb.search'); const { db } = await import('@/lib/db'); metaInfo = getCachedMetaInfo(rootPath); @@ -49,51 +49,105 @@ export async function GET(request: NextRequest) { } } } catch (error) { - console.error('[Detail] 从数据库读取 metainfo 失败:', error); + // 忽略错误 } - // 2. 调用 openlist detail API - const openlistResponse = await fetch( - `${request.headers.get('x-forwarded-proto') || 'http'}://${request.headers.get('host')}/api/openlist/detail?folder=${encodeURIComponent(id)}`, - { - headers: { - Cookie: request.headers.get('cookie') || '', - }, - } + // 2. 直接调用 OpenList 客户端获取视频列表 + const { OpenListClient } = await import('@/lib/openlist.client'); + const { getCachedVideoInfo, setCachedVideoInfo } = await import('@/lib/openlist-cache'); + const { parseVideoFileName } = await import('@/lib/video-parser'); + + const client = new OpenListClient( + openListConfig.URL, + openListConfig.Username, + openListConfig.Password ); - if (!openlistResponse.ok) { - throw new Error('获取 OpenList 视频详情失败'); + let videoInfo = getCachedVideoInfo(folderPath); + + if (!videoInfo) { + try { + const videoinfoPath = `${folderPath}/videoinfo.json`; + const fileResponse = await client.getFile(videoinfoPath); + + if (fileResponse.code === 200 && fileResponse.data.raw_url) { + const contentResponse = await fetch(fileResponse.data.raw_url); + const content = await contentResponse.text(); + videoInfo = JSON.parse(content); + if (videoInfo) { + setCachedVideoInfo(folderPath, videoInfo); + } + } + } catch (error) { + // 忽略错误 + } } - const openlistData = await openlistResponse.json(); + const listResponse = await client.listDirectory(folderPath); - if (!openlistData.success) { - throw new Error(openlistData.error || '获取视频详情失败'); + if (listResponse.code !== 200) { + throw new Error('OpenList 列表获取失败'); } + const videoExtensions = ['.mp4', '.mkv', '.avi', '.m3u8', '.flv', '.ts', '.mov', '.wmv', '.webm', '.rmvb', '.rm', '.mpg', '.mpeg', '.3gp', '.f4v', '.m4v', '.vob']; + const videoFiles = listResponse.data.content.filter((item) => { + if (item.is_dir || item.name.startsWith('.') || item.name.endsWith('.json')) return false; + return videoExtensions.some(ext => item.name.toLowerCase().endsWith(ext)); + }); + + if (!videoInfo) { + videoInfo = { episodes: {}, last_updated: Date.now() }; + videoFiles.sort((a, b) => a.name.localeCompare(b.name)); + for (let i = 0; i < videoFiles.length; i++) { + const file = videoFiles[i]; + const parsed = parseVideoFileName(file.name); + videoInfo.episodes[file.name] = { + episode: parsed.episode || (i + 1), + season: parsed.season, + title: parsed.title, + parsed_from: 'filename', + }; + } + setCachedVideoInfo(folderPath, videoInfo); + } + + const episodes = videoFiles + .map((file, index) => { + const parsed = parseVideoFileName(file.name); + let episodeInfo; + if (parsed.episode) { + episodeInfo = { episode: parsed.episode, season: parsed.season, title: parsed.title, parsed_from: 'filename' }; + } else { + episodeInfo = videoInfo!.episodes[file.name] || { episode: index + 1, season: undefined, title: undefined, parsed_from: 'filename' }; + } + let displayTitle = episodeInfo.title; + if (!displayTitle && episodeInfo.episode) { + displayTitle = `第${episodeInfo.episode}集`; + } + if (!displayTitle) { + displayTitle = file.name; + } + return { fileName: file.name, episode: episodeInfo.episode || 0, season: episodeInfo.season, title: displayTitle }; + }) + .sort((a, b) => a.episode !== b.episode ? a.episode - b.episode : a.fileName.localeCompare(b.fileName)); + // 3. 从 metainfo 中获取元数据 const folderMeta = metaInfo?.folders?.[id]; const { getTMDBImageUrl } = await import('@/lib/tmdb.search'); - // 转换为标准格式(使用懒加载 URL) const result = { source: 'openlist', source_name: '私人影库', - id: openlistData.folder, - title: folderMeta?.title || openlistData.folder, + id: id, + title: folderMeta?.title || id, poster: folderMeta?.poster_path ? getTMDBImageUrl(folderMeta.poster_path) : '', year: folderMeta?.release_date ? folderMeta.release_date.split('-')[0] : '', douban_id: 0, desc: folderMeta?.overview || '', - episodes: openlistData.episodes.map((ep: any) => - `/api/openlist/play?folder=${encodeURIComponent(openlistData.folder)}&fileName=${encodeURIComponent(ep.fileName)}` - ), - episodes_titles: openlistData.episodes.map((ep: any) => ep.title || `第${ep.episode}集`), + episodes: episodes.map((ep) => `/api/openlist/play?folder=${encodeURIComponent(id)}&fileName=${encodeURIComponent(ep.fileName)}`), + episodes_titles: episodes.map((ep) => ep.title), }; - console.log('[Detail] result.episodes_titles:', result.episodes_titles); - return NextResponse.json(result); } catch (error) { return NextResponse.json( From 8cf8eac55f97c689f3d49cfb093c17f13d0e607d Mon Sep 17 00:00:00 2001 From: mtvpls Date: Wed, 24 Dec 2025 09:55:58 +0800 Subject: [PATCH 2/4] fix typeerror --- src/app/admin/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 92943c3..130c3a3 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -507,6 +507,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP enabledApis?: string[]; tags?: string[]; created_at?: number; + oidcSub?: string; }> = !hasOldUserData && usersV2 ? usersV2 : (config?.UserConfig?.Users || []); // 使用 useMemo 计算全选状态,避免每次渲染都重新计算 From 21baa51fe7f183b6725325c8d3795cc7cc18cc5d Mon Sep 17 00:00:00 2001 From: mtvpls Date: Wed, 24 Dec 2025 10:29:26 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E7=A7=BB=E9=99=A4oidc=E5=AF=B9=E6=97=A7?= =?UTF-8?q?=E7=89=88=E6=B3=A8=E5=86=8C=E7=9A=84=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/auth/oidc/complete-register/route.ts | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/src/app/api/auth/oidc/complete-register/route.ts b/src/app/api/auth/oidc/complete-register/route.ts index 7825353..bd11e0f 100644 --- a/src/app/api/auth/oidc/complete-register/route.ts +++ b/src/app/api/auth/oidc/complete-register/route.ts @@ -161,27 +161,6 @@ export async function POST(request: NextRequest) { // 使用新版本创建用户(带SHA256加密和OIDC绑定) await db.createUserV2(username, randomPassword, 'user', defaultTags, oidcSession.sub); - // 同时在旧版本存储中创建(保持兼容性) - await db.registerUser(username, randomPassword); - - // 将用户添加到配置中(保持兼容性) - const newUser: any = { - username: username, - role: 'user', - banned: false, - oidcSub: oidcSession.sub, // 保存OIDC标识符 - }; - - // 如果配置了默认用户组,分配给新用户 - if (defaultTags) { - newUser.tags = defaultTags; - } - - config.UserConfig.Users.push(newUser); - - // 保存配置 - await db.saveAdminConfig(config); - // 设置认证cookie const response = NextResponse.json({ ok: true, message: '注册成功' }); const cookieValue = await generateAuthCookie(username, 'user'); From 9e6c98a8ff5aa0206970ef37e63e23eb92583337 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Wed, 24 Dec 2025 11:53:32 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E7=A7=81=E4=BA=BA=E5=BD=B1=E5=BA=93?= =?UTF-8?q?=E6=8D=A2=E6=BA=90=E4=B8=8D=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/EpisodeSelector.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/EpisodeSelector.tsx b/src/components/EpisodeSelector.tsx index 9a0b4d9..7e13ce5 100644 --- a/src/components/EpisodeSelector.tsx +++ b/src/components/EpisodeSelector.tsx @@ -266,7 +266,8 @@ const EpisodeSelector: React.FC = ({ if ( !optimizationEnabled || // 若关闭测速则直接退出 activeTab !== 'sources' || - availableSources.length === 0 + availableSources.length === 0 || + currentSource === 'openlist' // 私人影库不进行测速 ) return; @@ -293,7 +294,7 @@ const EpisodeSelector: React.FC = ({ fetchVideoInfosInBatches(); // 依赖项保持与之前一致 - }, [activeTab, availableSources, getVideoInfo, optimizationEnabled, initialTestingCompleted]); + }, [activeTab, availableSources, getVideoInfo, optimizationEnabled, initialTestingCompleted, currentSource]); // 升序分页标签 const categoriesAsc = useMemo(() => { @@ -848,6 +849,11 @@ const EpisodeSelector: React.FC = ({ {/* 重新测试按钮 */} {(() => { + // 私人影库不显示重新测试按钮 + if (source.source === 'openlist') { + return null; + } + const sourceKey = `${source.source}-${source.id}`; const isTesting = retestingSources.has(sourceKey); const videoInfo = videoInfoMap.get(sourceKey);