From a24c3f1a3256f99536701224c36bbca5f5f310be Mon Sep 17 00:00:00 2001 From: mtvpls Date: Mon, 8 Dec 2025 20:57:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=A7=82=E5=BD=B1=E5=AE=A4?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E4=B8=8B=E7=9A=84play=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/danmaku/comment/route.ts | 57 +++++++++++++------- src/app/api/danmaku/episodes/route.ts | 44 ++++++++++----- src/app/api/danmaku/match/route.ts | 45 +++++++++++----- src/app/api/danmaku/search/route.ts | 43 ++++++++++----- src/app/api/debug/watch-room-config/route.ts | 16 ++++-- src/app/play/page.tsx | 37 +++++-------- src/components/EpisodeSelector.tsx | 28 +++++++--- 7 files changed, 181 insertions(+), 89 deletions(-) diff --git a/src/app/api/danmaku/comment/route.ts b/src/app/api/danmaku/comment/route.ts index 480a907..0f8d05e 100644 --- a/src/app/api/danmaku/comment/route.ts +++ b/src/app/api/danmaku/comment/route.ts @@ -68,27 +68,46 @@ export async function GET(request: NextRequest) { apiUrl = `${baseUrl}/api/v2/comment?url=${encodeURIComponent(url!)}&format=xml`; } - const response = await fetch(apiUrl, { - method: 'GET', - headers: { - 'Accept': 'application/xml, text/xml', - }, - }); + // 添加超时控制 + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时 - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + try { + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Accept': 'application/xml, text/xml', + }, + signal: controller.signal, + keepalive: true, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + // 获取 XML 文本 + const xmlText = await response.text(); + + // 解析 XML 为 JSON + const comments = parseXmlDanmaku(xmlText); + + return NextResponse.json({ + count: comments.length, + comments, + }); + } catch (fetchError) { + clearTimeout(timeoutId); + + // 如果是超时错误,返回更友好的错误信息 + if (fetchError instanceof Error && fetchError.name === 'AbortError') { + throw new Error('弹幕服务器请求超时,请稍后重试'); + } + + throw fetchError; } - - // 获取 XML 文本 - const xmlText = await response.text(); - - // 解析 XML 为 JSON - const comments = parseXmlDanmaku(xmlText); - - return NextResponse.json({ - count: comments.length, - comments, - }); } catch (error) { console.error('获取弹幕代理错误:', error); return NextResponse.json( diff --git a/src/app/api/danmaku/episodes/route.ts b/src/app/api/danmaku/episodes/route.ts index 488be41..112ee85 100644 --- a/src/app/api/danmaku/episodes/route.ts +++ b/src/app/api/danmaku/episodes/route.ts @@ -38,20 +38,40 @@ export async function GET(request: NextRequest) { const apiUrl = `${baseUrl}/api/v2/bangumi/${animeId}`; - const response = await fetch(apiUrl, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); + // 添加超时控制和重试机制 + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时 - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + try { + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + signal: controller.signal, + // 添加 keepalive 避免连接被重置 + keepalive: true, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + return NextResponse.json(data); + } catch (fetchError) { + clearTimeout(timeoutId); + + // 如果是超时错误,返回更友好的错误信息 + if (fetchError instanceof Error && fetchError.name === 'AbortError') { + throw new Error('弹幕服务器请求超时,请稍后重试'); + } + + throw fetchError; } - - const data = await response.json(); - - return NextResponse.json(data); } catch (error) { console.error('获取剧集列表代理错误:', error); return NextResponse.json( diff --git a/src/app/api/danmaku/match/route.ts b/src/app/api/danmaku/match/route.ts index d678524..194a66c 100644 --- a/src/app/api/danmaku/match/route.ts +++ b/src/app/api/danmaku/match/route.ts @@ -35,21 +35,40 @@ export async function POST(request: NextRequest) { const apiUrl = `${baseUrl}/api/v2/match`; - const response = await fetch(apiUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ fileName }), - }); + // 添加超时控制 + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时 - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + try { + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ fileName }), + signal: controller.signal, + keepalive: true, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + return NextResponse.json(data); + } catch (fetchError) { + clearTimeout(timeoutId); + + // 如果是超时错误,返回更友好的错误信息 + if (fetchError instanceof Error && fetchError.name === 'AbortError') { + throw new Error('弹幕服务器请求超时,请稍后重试'); + } + + throw fetchError; } - - const data = await response.json(); - - return NextResponse.json(data); } catch (error) { console.error('自动匹配代理错误:', error); return NextResponse.json( diff --git a/src/app/api/danmaku/search/route.ts b/src/app/api/danmaku/search/route.ts index 4924fef..670381a 100644 --- a/src/app/api/danmaku/search/route.ts +++ b/src/app/api/danmaku/search/route.ts @@ -34,20 +34,39 @@ export async function GET(request: NextRequest) { const apiUrl = `${baseUrl}/api/v2/search/anime?keyword=${encodeURIComponent(keyword)}`; - const response = await fetch(apiUrl, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); + // 添加超时控制 + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时 - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + try { + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + signal: controller.signal, + keepalive: true, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + return NextResponse.json(data); + } catch (fetchError) { + clearTimeout(timeoutId); + + // 如果是超时错误,返回更友好的错误信息 + if (fetchError instanceof Error && fetchError.name === 'AbortError') { + throw new Error('弹幕服务器请求超时,请稍后重试'); + } + + throw fetchError; } - - const data = await response.json(); - - return NextResponse.json(data); } catch (error) { console.error('弹幕搜索代理错误:', error); return NextResponse.json( diff --git a/src/app/api/debug/watch-room-config/route.ts b/src/app/api/debug/watch-room-config/route.ts index f60e73b..d21f201 100644 --- a/src/app/api/debug/watch-room-config/route.ts +++ b/src/app/api/debug/watch-room-config/route.ts @@ -18,15 +18,23 @@ export async function GET(request: NextRequest) { hasUpstashUrl: !!process.env.UPSTASH_REDIS_REST_URL, hasUpstashToken: !!process.env.UPSTASH_REDIS_REST_TOKEN, hasKvrocksUrl: !!process.env.KVROCKS_URL, + watchRoomEnabled: process.env.WATCH_ROOM_ENABLED, + watchRoomServerType: process.env.WATCH_ROOM_SERVER_TYPE, + hasWatchRoomExternalUrl: !!process.env.WATCH_ROOM_EXTERNAL_SERVER_URL, + hasWatchRoomExternalAuth: !!process.env.WATCH_ROOM_EXTERNAL_SERVER_AUTH, + }, + watchRoomConfig: { + enabled: process.env.WATCH_ROOM_ENABLED === 'true', + serverType: process.env.WATCH_ROOM_SERVER_TYPE || 'internal', + externalServerUrl: process.env.WATCH_ROOM_EXTERNAL_SERVER_URL, + externalServerAuth: process.env.WATCH_ROOM_EXTERNAL_SERVER_AUTH ? '***' : undefined, }, - watchRoomConfig: null as any, configReadError: null as string | null, }; - // 尝试读取配置 + // 尝试读取配置(验证数据库连接) try { - const config = await getConfig(); - debugInfo.watchRoomConfig = config.WatchRoomConfig || null; + await getConfig(); } catch (error) { debugInfo.configReadError = (error as Error).message; } diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index 363ed2e..a1cb0b6 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -1976,7 +1976,7 @@ function PlayPageClient() { // 检查是否有记忆 const memory = loadDanmakuMemory(title); if (memory) { - console.log('[弹幕] 找到记忆 - 视频:', title, '→ 弹幕源:', memory.animeTitle); + console.log('[弹幕] 找到缓存 - 视频:', title, '→ 弹幕源:', memory.animeTitle); // 获取该动漫的所有剧集列表 try { @@ -2011,13 +2011,14 @@ function PlayPageClient() { memory.searchKeyword // 保留原有的搜索关键词 ); + console.log('[弹幕] 使用缓存成功,跳过搜索'); await loadDanmaku(episode.episodeId); - return; + return; // 成功使用缓存,直接返回 } } - // 如果使用记忆加载失败,清除该记忆并继续自动搜索 - console.warn('[弹幕] 使用缓存加载失败,清除缓存并从头搜索'); + // 如果使用记忆加载失败(没有找到对应的剧集),清除该记忆并继续自动搜索 + console.warn('[弹幕] 缓存中没有找到对应剧集,清除缓存并重新搜索'); if (artPlayerRef.current) { artPlayerRef.current.notice.show = '缓存的弹幕源失效,正在重新搜索...'; } @@ -2029,10 +2030,10 @@ function PlayPageClient() { const memories = JSON.parse(memoriesJson); delete memories[title]; localStorage.setItem('danmaku_memories', JSON.stringify(memories)); - console.log('[弹幕] 已清除失效的缓存记忆'); + console.log('[弹幕] 已清除失效的缓存'); } } catch (e) { - console.error('[弹幕] 清除缓存记忆失败:', e); + console.error('[弹幕] 清除缓存失败:', e); } } } catch (error) { @@ -2048,14 +2049,14 @@ function PlayPageClient() { const memories = JSON.parse(memoriesJson); delete memories[title]; localStorage.setItem('danmaku_memories', JSON.stringify(memories)); - console.log('[弹幕] 已清除失效的缓存记忆'); + console.log('[弹幕] 已清除失效的缓存'); } } catch (e) { - console.error('[弹幕] 清除缓存记忆失败:', e); + console.error('[弹幕] 清除缓存失败:', e); } } } - // 继续执行后面的自动搜索逻辑,不要 return + // 如果缓存加载失败,继续执行后面的自动搜索逻辑 } // 自动搜索弹幕 @@ -3534,8 +3535,8 @@ function PlayPageClient() { - {/* 第三方应用打开按钮 */} - {videoUrl && ( + {/* 第三方应用打开按钮 - 观影室同步状态下隐藏 */} + {videoUrl && !playSync.isInRoom && (
@@ -3802,29 +3803,19 @@ function PlayPageClient() { {/* 选集和换源 - 在移动端始终显示,在 lg 及以上可折叠 */}
- {/* 观影室房员禁用层 */} - {playSync.isInRoom && playSync.shouldDisableControls && ( -
-
-

👥 观影室模式

-

- {playSync.isOwner ? '您是房主,可以控制播放' : '房主控制中,无法切换集数和播放源'} -

-
-
- )} { /* disabled */ } : handleEpisodeChange} onSourceChange={playSync.shouldDisableControls ? () => { /* disabled */ } : handleSourceChange} + isRoomMember={playSync.shouldDisableControls} currentSource={currentSource} currentId={currentId} videoTitle={searchTitle || videoTitle} diff --git a/src/components/EpisodeSelector.tsx b/src/components/EpisodeSelector.tsx index 0ac36cf..784afcf 100644 --- a/src/components/EpisodeSelector.tsx +++ b/src/components/EpisodeSelector.tsx @@ -47,6 +47,8 @@ interface EpisodeSelectorProps { /** 弹幕相关 */ onDanmakuSelect?: (selection: DanmakuSelection) => void; currentDanmakuSelection?: DanmakuSelection | null; + /** 观影室房员状态 - 禁用选集和换源,但保留弹幕 */ + isRoomMember?: boolean; } /** @@ -68,6 +70,7 @@ const EpisodeSelector: React.FC = ({ precomputedVideoInfo, onDanmakuSelect, currentDanmakuSelection, + isRoomMember = false, }) => { const router = useRouter(); const pageCount = Math.ceil(totalEpisodes / episodesPerPage); @@ -94,8 +97,17 @@ const EpisodeSelector: React.FC = ({ }, [videoInfoMap]); // 主要的 tab 状态:'danmaku' | 'episodes' | 'sources' - // 默认显示选集选项卡 - const [activeTab, setActiveTab] = useState<'danmaku' | 'episodes' | 'sources'>('episodes'); + // 默认显示选集选项卡,但如果是房员则显示弹幕 + const [activeTab, setActiveTab] = useState<'danmaku' | 'episodes' | 'sources'>( + isRoomMember ? 'danmaku' : 'episodes' + ); + + // 当房员状态变化时,自动切换到弹幕选项卡 + useEffect(() => { + if (isRoomMember && (activeTab === 'episodes' || activeTab === 'sources')) { + setActiveTab('danmaku'); + } + }, [isRoomMember, activeTab]); // 当前分页索引(0 开始) const initialPage = Math.floor((value - 1) / episodesPerPage); @@ -360,8 +372,9 @@ const EpisodeSelector: React.FC = ({ {/* 选集选项卡 - 仅在多集时显示 */} {totalEpisodes > 1 && (
setActiveTab('episodes')} - className={`flex-1 py-3 px-6 text-center cursor-pointer transition-all duration-200 font-medium + onClick={() => !isRoomMember && setActiveTab('episodes')} + className={`flex-1 py-3 px-6 text-center transition-all duration-200 font-medium relative + ${isRoomMember ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'} ${activeTab === 'episodes' ? 'text-green-600 dark:text-green-400' : 'text-gray-700 hover:text-green-600 bg-black/5 dark:bg-white/5 dark:text-gray-300 dark:hover:text-green-400 hover:bg-black/3 dark:hover:bg-white/3' @@ -369,13 +382,15 @@ const EpisodeSelector: React.FC = ({ `.trim()} > 选集 + {isRoomMember && 🔒}
)} {/* 换源选项卡 */}
!isRoomMember && handleSourceTabClick()} + className={`flex-1 py-3 px-6 text-center transition-all duration-200 font-medium relative + ${isRoomMember ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'} ${activeTab === 'sources' ? 'text-green-600 dark:text-green-400' : 'text-gray-700 hover:text-green-600 bg-black/5 dark:bg-white/5 dark:text-gray-300 dark:hover:text-green-400 hover:bg-black/3 dark:hover:bg-white/3' @@ -383,6 +398,7 @@ const EpisodeSelector: React.FC = ({ `.trim()} > 换源 + {isRoomMember && 🔒}
{/* 弹幕选项卡 */}