From 63594da9ec5df86865b45c29ab9aa6b248977d29 Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 08:25:25 +0800 Subject: [PATCH 01/18] fix(player): apply playback rate change through orchestrator when cycling speeds (#210) Fixed an issue where clicking the playback speed button to cycle through favorite speeds would update the store state but not actually change the video playback rate. The problem was that cycleFavoriteRate() only updated the Zustand store without going through the PlayerOrchestrator. According to the project's architecture, all playback control operations must be executed through the orchestrator to properly sync with the video element. Menu clicks worked because they called setSpeed() which properly invokes orchestrator.requestSetPlaybackRate(). Changes: - Calculate next favorite speed before calling cycleFavoriteRate() - Call setSpeed() after updating store to apply rate through orchestrator - Maintains same code path as menu selection for consistency --- .../ControllerPanel/controls/PlaybackRateControl.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/renderer/src/pages/player/components/ControllerPanel/controls/PlaybackRateControl.tsx b/src/renderer/src/pages/player/components/ControllerPanel/controls/PlaybackRateControl.tsx index 99302ac2..277a2925 100644 --- a/src/renderer/src/pages/player/components/ControllerPanel/controls/PlaybackRateControl.tsx +++ b/src/renderer/src/pages/player/components/ControllerPanel/controls/PlaybackRateControl.tsx @@ -47,7 +47,16 @@ export default function PlaybackRateControl() { // 处理左键点击:循环切换常用速度 const handleLeftClick = () => { if (favoriteRates.length > 0) { + // 计算下一个常用速度 + const currentIndex = usePlayerStore.getState().currentFavoriteIndex + const nextIndex = (currentIndex + 1) % favoriteRates.length + const nextRate = favoriteRates[nextIndex] + + // 先更新 store 状态 cycleFavoriteRate() + + // 再通过 orchestrator 应用到视频 + setSpeed(nextRate) } else { // 如果没有常用速度,使用默认逻辑(打开菜单) toggleMenu() From 6fc1916a3631ceaa3548527864d487db49dba98b Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 09:12:11 +0800 Subject: [PATCH 02/18] feat(player): HLS session progress polling with media server integration (#209) * feat(player): implement HLS session progress polling with visual feedback - Add waitForSessionReady to poll session creation progress before playback - Integrate useSessionProgress hook for reusable progress tracking logic - Display deterministic progress bar with percentage and stage information during session initialization - Add SessionService.getSessionProgress API with HTTP 425 handling for in-progress state - Update backend submodule to v64aaf0b with HLS session progress support - Remove excessive debug logging in SubtitleOverlay to reduce console noise Changes: PlayerPage: - Add waitForSessionReady state and sessionProgress state for UI rendering - Implement polling loop with 2s interval to fetch session progress until ready - Display progress bar with stage text and percentage during session creation - Handle HTTP 425 (session not ready) gracefully in progress polling - Fallback to default playlist URL if progress response URL parsing fails useSessionProgress Hook: - Provide reusable session progress polling with configurable interval (default 2s) - Auto-stop polling when session is ready or error occurs - Expose startPolling, stopPolling, reset methods for lifecycle control - Implement progress threshold (5%) to reduce log noise SessionService: - Add getSessionProgress(sessionId) API to fetch session creation progress - Define SessionProgressResponse and AudioProgressInfo interfaces - Treat HTTP 425 as normal in-progress state (not error) This enhancement provides real-time visual feedback during HLS session initialization, improving user experience by showing progress instead of generic "loading..." spinner. The polling mechanism ensures playback starts only after the session is fully ready, preventing premature playlist access. * refactor(PlayerPage): replace progress bar hardcoded values with theme tokens - Import theme constants (SPACING, FONT_SIZES, ANIMATION_DURATION, etc.) - Replace hardcoded width (240px) with SPACING.XXL * 5 - Replace hardcoded heights (4px) with COMPONENT_TOKENS.PROGRESS_BAR.TRACK_HEIGHT_HOVER - Replace hardcoded border-radius (2px) with COMPONENT_TOKENS.PROGRESS_BAR.TRACK_BORDER_RADIUS - Replace hardcoded font-sizes (16px, 14px) with FONT_SIZES.BASE and FONT_SIZES.SM - Replace hardcoded transition timing with ANIMATION_DURATION.SLOW and EASING.STANDARD - Fix: Add setTranscodeStatus('completed') call after HLS session completes Changes ensure theme consistency, maintainability, and proper status tracking across components. Progress bar now respects design system tokens for cross-theme compatibility. --- backend | 2 +- src/renderer/src/pages/player/PlayerPage.tsx | 231 +++++++++++++++--- .../player/components/SubtitleOverlay.tsx | 9 - src/renderer/src/services/SessionService.ts | 63 +++++ 4 files changed, 262 insertions(+), 43 deletions(-) diff --git a/backend b/backend index 176ca823..64aaf0bd 160000 --- a/backend +++ b/backend @@ -1 +1 @@ -Subproject commit 176ca823ac2e6365f0721e43725c633b2d5aa9da +Subproject commit 64aaf0bd4d1c1d15bd703bbd5ac00c2034bb2e4b diff --git a/src/renderer/src/pages/player/PlayerPage.tsx b/src/renderer/src/pages/player/PlayerPage.tsx index 7ae50f02..cefbd268 100644 --- a/src/renderer/src/pages/player/PlayerPage.tsx +++ b/src/renderer/src/pages/player/PlayerPage.tsx @@ -4,6 +4,7 @@ import db from '@renderer/databases' import { CodecCompatibilityChecker, type ExtendedErrorType, + SessionError, SessionService, VideoLibraryService } from '@renderer/services' @@ -16,6 +17,13 @@ import { Layout, Tooltip } from 'antd' const { Content, Sider } = Layout import { MediaServerRecommendationPrompt } from '@renderer/components/MediaServerRecommendationPrompt' +import { + ANIMATION_DURATION, + COMPONENT_TOKENS, + EASING, + FONT_SIZES, + SPACING +} from '@renderer/infrastructure/styles/theme' import { ArrowLeft, PanelRightClose, PanelRightOpen } from 'lucide-react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -95,6 +103,12 @@ function PlayerPage() { originalPath?: string } | null>(null) const [showMediaServerPrompt, setShowMediaServerPrompt] = useState(false) + const [waitingForSessionReady, setWaitingForSessionReady] = useState(false) + const [sessionProgress, setSessionProgress] = useState<{ + percent: number + stage: string + status: string + } | null>(null) // const { pokeInteraction } = usePlayerUI() // 保存转码会话 ID 用于清理 @@ -103,8 +117,64 @@ function PlayerPage() { // 加载视频数据 useEffect(() => { let cancelled = false + const pollIntervalMs = 2000 + + const waitForSessionReady = async (sessionId: string) => { + while (!cancelled) { + try { + const progress = await SessionService.getSessionProgress(sessionId) + if (cancelled) { + break + } + + setSessionProgress((prev) => { + const stage = progress.progress_stage?.trim() || prev?.stage || '处理中...' + const rawPercent = + typeof progress.progress_percent === 'number' + ? progress.progress_percent + : Number(progress.progress_percent) + const percent = Number.isFinite(rawPercent) ? rawPercent : (prev?.percent ?? 0) + return { + percent, + stage, + status: progress.status + } + }) + + if (progress.is_ready) { + setSessionProgress((prev) => ({ + percent: 100, + stage: progress.progress_stage?.trim() || prev?.stage || '就绪', + status: progress.status + })) + return progress + } + } catch (progressError) { + if ( + progressError instanceof SessionError && + progressError.statusCode && + progressError.statusCode === 425 + ) { + // 会话尚未返回进度,等待下一轮 + } else { + throw progressError + } + } + + if (cancelled) { + break + } + + await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)) + } + + throw new Error('会话进度轮询已取消') + } + const loadData = async () => { setLoading(true) + setWaitingForSessionReady(false) + setSessionProgress(null) if (!videoId) { setError('无效的视频 ID') setVideoData(null) @@ -189,36 +259,83 @@ function PlayerPage() { // 保存会话 ID 用于后续清理 sessionIdRef.current = sessionResult.session_id - - // 构建完整的播放列表 URL - const playListUrl = await SessionService.getPlaylistUrl(sessionResult.session_id) - - // 更新转码信息和播放源 - usePlayerStore.getState().updateTranscodeInfo({ - hlsSrc: playListUrl, - windowId: 0, // 会话模式不再使用 windowId - assetHash: sessionResult.asset_hash, - profileHash: sessionResult.profile_hash, - cached: false, // 会话模式由后端管理缓存 - sessionId: sessionResult.session_id, - endTime: Date.now() + setWaitingForSessionReady(true) + setSessionProgress({ + percent: 0, + stage: '正在创建转码会话...', + status: 'initializing' }) - // 切换到 HLS 播放模式 - usePlayerStore.getState().switchToHlsSource(playListUrl, { - windowId: 0, - assetHash: sessionResult.asset_hash, - profileHash: sessionResult.profile_hash, - cached: false, - sessionId: sessionResult.session_id - }) - - finalSrc = playListUrl - - logger.info('预转码流程完成,使用 HLS 播放源', { - originalSrc: fileUrl, - hlsSrc: finalSrc - }) + try { + const readyProgress = await waitForSessionReady(sessionResult.session_id) + + if (cancelled) { + return + } + + const fallbackPlaylistUrl = await SessionService.getPlaylistUrl( + sessionResult.session_id + ) + let playListUrl = fallbackPlaylistUrl + + if (readyProgress.playlist_url) { + try { + playListUrl = new URL( + readyProgress.playlist_url, + fallbackPlaylistUrl + ).toString() + } catch (urlError) { + logger.warn('解析会话进度返回的播放列表 URL 失败,将使用默认值', { + sessionId: sessionResult.session_id, + playlistUrl: readyProgress.playlist_url, + error: urlError instanceof Error ? urlError.message : String(urlError) + }) + playListUrl = fallbackPlaylistUrl + } + } + + // 更新转码信息和播放源 + usePlayerStore.getState().updateTranscodeInfo({ + hlsSrc: playListUrl, + windowId: 0, // 会话模式不再使用 windowId + assetHash: sessionResult.asset_hash, + profileHash: sessionResult.profile_hash, + cached: false, // 会话模式由后端管理缓存 + sessionId: sessionResult.session_id, + endTime: Date.now() + }) + + // 切换到 HLS 播放模式 + usePlayerStore.getState().switchToHlsSource(playListUrl, { + windowId: 0, + assetHash: sessionResult.asset_hash, + profileHash: sessionResult.profile_hash, + cached: false, + sessionId: sessionResult.session_id + }) + if (!cancelled) { + usePlayerStore.getState().setTranscodeStatus('completed') + } + + finalSrc = playListUrl + + logger.info('预转码流程完成,使用 HLS 播放源', { + originalSrc: fileUrl, + hlsSrc: finalSrc + }) + } catch (progressError) { + if (!cancelled) { + const message = + progressError instanceof Error ? progressError.message : '获取会话进度失败' + logger.error('会话进度轮询失败,转码流程终止', { + error: message, + sessionId: sessionResult.session_id + }) + setError(message || '获取会话进度失败') + usePlayerStore.getState().setTranscodeStatus('failed') + } + return + } } } catch (checkError) { logger.error('检查 Media Server 状态失败,显示推荐安装弹窗', { @@ -270,7 +387,10 @@ function PlayerPage() { logger.error(`加载视频数据失败: ${err}`) setError(err instanceof Error ? err.message : '加载失败') } finally { - if (!cancelled) setLoading(false) + if (!cancelled) { + setWaitingForSessionReady(false) + setLoading(false) + } } } @@ -415,13 +535,29 @@ function PlayerPage() { }, [handleToggleFullscreen]) if (loading) { + const progressPercent = Math.max(0, Math.min(100, Math.round(sessionProgress?.percent ?? 0))) + return ( - 加载中... - - - + {waitingForSessionReady ? ( + <> + + {sessionProgress?.stage || '正在创建转码会话...'} + + + + + {progressPercent}% + + ) : ( + <> + 加载中... + + + + + )} ) @@ -609,6 +745,35 @@ const LoadingBarProgress = styled.div` } ` +const DETERMINATE_BAR_WIDTH = SPACING.XXL * 5 + +const DeterminateBarTrack = styled.div` + width: ${DETERMINATE_BAR_WIDTH}px; + height: ${COMPONENT_TOKENS.PROGRESS_BAR.TRACK_HEIGHT_HOVER}px; + background: var(--ant-color-fill-quaternary, rgba(255, 255, 255, 0.08)); + border-radius: ${COMPONENT_TOKENS.PROGRESS_BAR.TRACK_BORDER_RADIUS}px; + overflow: hidden; +` + +const DeterminateBarFill = styled.div<{ $percent: number }>` + height: 100%; + width: ${({ $percent }) => `${$percent}%`}; + background: var(--ant-color-primary, #1677ff); + border-radius: ${COMPONENT_TOKENS.PROGRESS_BAR.TRACK_BORDER_RADIUS}px; + transition: width ${ANIMATION_DURATION.SLOW} ${EASING.STANDARD}; +` + +const ProgressStageText = styled.div` + font-size: ${FONT_SIZES.BASE}px; + color: var(--color-text-1, #ddd); + text-align: center; +` + +const ProgressPercentText = styled.div` + font-size: ${FONT_SIZES.SM}px; + color: var(--color-text-2, #bbb); +` + const ErrorContainer = styled.div` display: flex; flex-direction: column; diff --git a/src/renderer/src/pages/player/components/SubtitleOverlay.tsx b/src/renderer/src/pages/player/components/SubtitleOverlay.tsx index 110dabdd..8778a6c6 100644 --- a/src/renderer/src/pages/player/components/SubtitleOverlay.tsx +++ b/src/renderer/src/pages/player/components/SubtitleOverlay.tsx @@ -423,15 +423,6 @@ export const SubtitleOverlay = memo(function SubtitleOverlay({ return null } - logger.debug('渲染 SubtitleOverlay', { - displayMode, - position, - size, - isDragging, - isResizing, - showBoundaries - }) - return ( { + logger.debug('获取会话进度', { sessionId }) + + try { + const response = await this.makeRequest(`/${sessionId}/progress`) + + logger.debug('会话进度获取成功', { + sessionId, + status: response.status, + progress: response.progress_percent, + stage: response.progress_stage, + isReady: response.is_ready + }) + + return response + } catch (error) { + // 如果是 HTTP 425 (Too Early),这是正常的进行中状态,不记录为错误 + if (error instanceof SessionError && error.statusCode === 425) { + logger.debug('会话尚未就绪 (HTTP 425)', { sessionId }) + throw error + } + + logger.error('获取会话进度失败', { + sessionId, + error: error instanceof Error ? error.message : String(error) + }) + throw error + } + } + /** * 获取当前活跃的请求数量(用于监控) */ From 11c8d94530880f1b641fe747cb8e60c5716f63e0 Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 19:53:21 +0800 Subject: [PATCH 03/18] feat(media-server): add transcode cache cleanup for deleted videos - Add MediaServer_CleanupCachesForFile IPC channel in shared types - Implement cleanupCachesForFile method in MediaServerService with asset hash calculation - Add TranscodeCacheCleanupResult type for cleanup operation results - Register IPC handler in main process to expose cleanup functionality - Expose cleanupCachesForFile API to renderer through preload bridge - Integrate cache cleanup into HomePage video deletion flow Changes: - IpcChannel: Add MediaServer_CleanupCachesForFile channel - MediaServerService: Add cleanupCachesForFile, calculateAssetHash, removeDirectoryIfExists methods - ipc.ts: Register handler for cleanup cache IPC channel - preload: Expose cleanupCachesForFile to renderer API - HomePage: Lookup video file path and trigger cache cleanup after deletion This enhancement ensures HLS segments and audio cache directories are automatically removed when users delete videos from the library, preventing unnecessary disk space usage and maintaining cache hygiene. --- packages/shared/IpcChannel.ts | 1 + src/main/ipc.ts | 3 + src/main/services/MediaServerService.ts | 109 +++++++++++++++++++++++ src/preload/index.ts | 4 +- src/renderer/src/pages/home/HomePage.tsx | 32 +++++++ 5 files changed, 148 insertions(+), 1 deletion(-) diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 8d753df7..438dd4ed 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -126,6 +126,7 @@ export enum IpcChannel { MediaServer_GetInfo = 'media-server:get-info', MediaServer_GetPort = 'media-server:get-port', MediaServer_CheckHealth = 'media-server:check-health', + MediaServer_CleanupCachesForFile = 'media-server:cleanup-caches-for-file', MediaServer_PortChanged = 'media-server:port-changed', // 端口变更事件 // MediaInfo 相关 IPC 通道 / MediaInfo related IPC channels diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 7ec9d232..fedd248e 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -651,6 +651,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.MediaServer_GetPort, async () => { return mediaServerService.getPort() }) + ipcMain.handle(IpcChannel.MediaServer_CleanupCachesForFile, async (_, filePath: string) => { + return await mediaServerService.cleanupCachesForFile(filePath) + }) // MediaParser (Remotion) ipcMain.handle(IpcChannel.MediaInfo_CheckExists, async () => { diff --git a/src/main/services/MediaServerService.ts b/src/main/services/MediaServerService.ts index b32f6245..cde0bb7f 100644 --- a/src/main/services/MediaServerService.ts +++ b/src/main/services/MediaServerService.ts @@ -1,3 +1,7 @@ +import { createHash } from 'node:crypto' +import fs from 'node:fs' +import { open, rm, stat } from 'node:fs/promises' + import { IpcChannel } from '@shared/IpcChannel' import type { MediaServerInfo, MediaServerStatus } from '@shared/types' import { ChildProcess, spawn } from 'child_process' @@ -54,6 +58,20 @@ export interface MediaServerConfig { audioTrackTtlHours?: number // 音频轨道 TTL(小时) } +export interface TranscodeCacheCleanupResult { + assetHash: string + hls: { + path: string + removed: boolean + error?: string + } + audio: { + path: string + removed: boolean + error?: string + } +} + /** * Media Server 管理服务 * 负责启动、停止和监控 Media Server 进程 @@ -513,6 +531,97 @@ export class MediaServerService { return this.port } + /** + * 清理指定文件对应的转码缓存 + * @param filePath 原始视频文件路径 + */ + public async cleanupCachesForFile(filePath: string): Promise { + const resolvedPath = path.resolve(filePath) + if (!fs.existsSync(resolvedPath)) { + throw new Error(`文件不存在: ${resolvedPath}`) + } + + const assetHash = await this.calculateAssetHash(resolvedPath) + const dataPath = getDataPath() + const mediaServerCachePath = path.join(dataPath, 'MediaServerCache') + const hlsDir = path.join(mediaServerCachePath, 'hls-segments', assetHash) + const audioDir = path.join(mediaServerCachePath, 'audio-cache', assetHash) + + logger.info('清理转码缓存', { + filePath: resolvedPath, + assetHash, + hlsDir, + audioDir + }) + + const hlsResult = await this.removeDirectoryIfExists(hlsDir) + const audioResult = await this.removeDirectoryIfExists(audioDir) + + return { + assetHash, + hls: { + path: hlsDir, + ...hlsResult + }, + audio: { + path: audioDir, + ...audioResult + } + } + } + + private async calculateAssetHash(filePath: string): Promise { + const resolvedPath = path.resolve(filePath) + const fileStat = await stat(resolvedPath) + const hash = createHash('sha256') + + hash.update(resolvedPath) + hash.update(fileStat.size.toString()) + hash.update(Math.floor(fileStat.mtimeMs / 1000).toString()) + + const sampleSize = 8 * 1024 * 1024 + const fileSize = Number(fileStat.size) + const fileHandle = await open(resolvedPath, 'r') + + try { + if (fileSize <= sampleSize * 2) { + const buffer = Buffer.alloc(fileSize) + const { bytesRead } = await fileHandle.read(buffer, 0, buffer.length, 0) + hash.update(buffer.subarray(0, bytesRead)) + } else { + const headBuffer = Buffer.alloc(sampleSize) + await fileHandle.read(headBuffer, 0, sampleSize, 0) + hash.update(headBuffer) + + const tailBuffer = Buffer.alloc(sampleSize) + await fileHandle.read(tailBuffer, 0, sampleSize, fileSize - sampleSize) + hash.update(tailBuffer) + } + } finally { + await fileHandle.close() + } + + return hash.digest('hex').slice(0, 16) + } + + private async removeDirectoryIfExists( + dirPath: string + ): Promise<{ removed: boolean; error?: string }> { + try { + if (!fs.existsSync(dirPath)) { + return { removed: false } + } + + await rm(dirPath, { recursive: true, force: true }) + logger.info('已删除转码缓存目录', { dirPath }) + return { removed: true } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + logger.warn('删除转码缓存目录失败', { dirPath, error: message }) + return { removed: false, error: message } + } + } + /** * 等待服务器启动 */ diff --git a/src/preload/index.ts b/src/preload/index.ts index b528bcd1..13deba01 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -332,7 +332,9 @@ const api = { uptime?: number error?: string }> => ipcRenderer.invoke(IpcChannel.MediaServer_GetInfo), - getPort: (): Promise => ipcRenderer.invoke(IpcChannel.MediaServer_GetPort) + getPort: (): Promise => ipcRenderer.invoke(IpcChannel.MediaServer_GetPort), + cleanupCachesForFile: (filePath: string) => + ipcRenderer.invoke(IpcChannel.MediaServer_CleanupCachesForFile, filePath) }, fs: { checkFileExists: (filePath: string): Promise => diff --git a/src/renderer/src/pages/home/HomePage.tsx b/src/renderer/src/pages/home/HomePage.tsx index ac450d59..7ad8119a 100644 --- a/src/renderer/src/pages/home/HomePage.tsx +++ b/src/renderer/src/pages/home/HomePage.tsx @@ -1,5 +1,6 @@ import { loggerService } from '@logger' import FFmpegDownloadPrompt from '@renderer/components/FFmpegDownloadPrompt' +import FileManager from '@renderer/services/FileManager' import HomePageVideoService, { type HomePageVideoItem } from '@renderer/services/HomePageVideos' import { VideoLibraryService } from '@renderer/services/VideoLibrary' import { useSettingsStore } from '@renderer/state/stores/settings.store' @@ -164,6 +165,21 @@ export function HomePage(): React.JSX.Element { onOk: async () => { try { const videoLibraryService = new VideoLibraryService() + let filePath: string | null = null + + try { + const record = await videoLibraryService.getRecordById(video.id) + if (record?.fileId) { + const fileInfo = await FileManager.getFile(record.fileId) + filePath = fileInfo?.path ?? null + } + } catch (lookupError) { + logger.warn('获取视频文件信息失败,跳过转码缓存清理', { + error: lookupError, + videoId: video.id + }) + } + await videoLibraryService.deleteRecord(video.id) // 从本地状态中移除该视频 @@ -173,6 +189,22 @@ export function HomePage(): React.JSX.Element { setCachedVideos(cachedVideos.filter((v) => v.id !== video.id)) message.success(t('home.delete.success_message')) + + if (filePath) { + void window.api.mediaServer + .cleanupCachesForFile(filePath) + .then((result) => { + logger.info('已清理转码缓存', { result, videoId: video.id }) + }) + .catch((cleanupError) => { + logger.warn('清理转码缓存失败', { + error: + cleanupError instanceof Error ? cleanupError.message : String(cleanupError), + filePath, + videoId: video.id + }) + }) + } } catch (error) { logger.error('删除视频记录失败', { error }) message.error(t('home.delete.error_message')) From 76c351eb6965528827173fb735cb80e7eea94cbd Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 20:44:08 +0800 Subject: [PATCH 04/18] fix(player): remove HLS player missing error handling - remove 'hls-player-missing' error type from ExtendedErrorType - clean up related UI components and styled components - remove HLS-specific status display from VideoErrorRecovery modal --- .../player/components/VideoErrorRecovery.tsx | 88 ------------------- .../src/services/CodecCompatibilityChecker.ts | 1 - 2 files changed, 89 deletions(-) diff --git a/src/renderer/src/pages/player/components/VideoErrorRecovery.tsx b/src/renderer/src/pages/player/components/VideoErrorRecovery.tsx index 18567aba..997e2ce9 100644 --- a/src/renderer/src/pages/player/components/VideoErrorRecovery.tsx +++ b/src/renderer/src/pages/player/components/VideoErrorRecovery.tsx @@ -60,12 +60,6 @@ function VideoErrorRecovery({ title: t('player.errorRecovery.errors.networkError.title'), description: t('player.errorRecovery.errors.networkError.description') } - case 'hls-player-missing': - return { - title: 'HLS 播放器未就绪', - description: - '视频已成功转码,但当前版本尚未集成 HLS 播放器。HLS 播放器正在开发中,将在后续版本提供支持。' - } default: return { title: t('player.errorRecovery.errors.unknown.title'), @@ -178,22 +172,6 @@ function VideoErrorRecovery({ {videoTitle} {errorInfo.description} - - {errorType === 'hls-player-missing' && ( - - 转码状态 - ✅ 视频转码已完成,HLS 文件已生成 - ⏳ 等待 HLS 播放器实现 - 注:原始视频文件仍然可以在文件管理器中正常播放。 - - )} - - {originalPath && errorType !== 'hls-player-missing' && ( - - {t('player.errorRecovery.pathInfo.label')} - {originalPath} - - )} @@ -282,69 +260,3 @@ const ErrorDescription = styled.p` font-size: 14px; line-height: 1.6; ` - -const PathInfo = styled.div` - background: var(--color-background-soft); - border-radius: 8px; - padding: 16px; - margin-top: 16px; - border: 1px solid var(--color-border-soft); -` - -const PathLabel = styled.div` - font-size: 12px; - color: var(--color-text-3); - margin-bottom: 8px; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.5px; -` - -const PathValue = styled.div` - font-size: 13px; - color: var(--color-text-2); - font-family: 'Menlo', 'Monaco', 'Consolas', monospace; - word-break: break-all; - background: var(--color-background); - padding: 10px 12px; - border-radius: 6px; - border: 1px solid var(--color-border); -` - -const HlsStatusInfo = styled.div` - background: var(--color-background-soft); - border-radius: 8px; - padding: 16px; - margin-top: 16px; - border: 1px solid var(--color-border-soft); -` - -const StatusLabel = styled.div` - font-size: 12px; - color: var(--color-text-3); - margin-bottom: 12px; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.5px; -` - -const StatusValue = styled.div` - font-size: 14px; - color: var(--color-text-2); - margin-bottom: 8px; - display: flex; - align-items: center; - gap: 8px; - - &:last-of-type { - margin-bottom: 12px; - } -` - -const StatusNote = styled.div` - font-size: 12px; - color: var(--color-text-3); - font-style: italic; - padding-top: 8px; - border-top: 1px solid var(--color-border); -` diff --git a/src/renderer/src/services/CodecCompatibilityChecker.ts b/src/renderer/src/services/CodecCompatibilityChecker.ts index 02503662..a487495e 100644 --- a/src/renderer/src/services/CodecCompatibilityChecker.ts +++ b/src/renderer/src/services/CodecCompatibilityChecker.ts @@ -262,7 +262,6 @@ export type ExtendedErrorType = | 'audio-codec-unsupported' | 'video-codec-unsupported' | 'codec-unsupported' - | 'hls-player-missing' // HLS 播放器未就绪,转码已完成但无法播放 | 'hls-playback-error' // HLS 播放过程中的错误 | 'unknown' From 843b9b653678a9dc0279839fecd50623a0da3b4f Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 20:52:26 +0800 Subject: [PATCH 05/18] fix(codec-compatibility): handle missing codec information gracefully - trim video and audio codec strings to avoid whitespace issues - ensure compatibility checks handle undefined codec values - add missing codec reasons to incompatibility list - improve robustness of codec detection logic --- .../src/services/CodecCompatibilityChecker.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/services/CodecCompatibilityChecker.ts b/src/renderer/src/services/CodecCompatibilityChecker.ts index a487495e..936962da 100644 --- a/src/renderer/src/services/CodecCompatibilityChecker.ts +++ b/src/renderer/src/services/CodecCompatibilityChecker.ts @@ -376,7 +376,8 @@ export class CodecCompatibilityChecker { } } - const { videoCodec, audioCodec } = videoInfo + const videoCodec = videoInfo.videoCodec?.trim() + const audioCodec = videoInfo.audioCodec?.trim() logger.debug('检测到的编解码器信息', { videoCodec, @@ -386,20 +387,24 @@ export class CodecCompatibilityChecker { }) // 检查视频编解码器兼容性 - const videoSupported = this.checkVideoCodecSupport(videoCodec) + const videoSupported = videoCodec ? this.checkVideoCodecSupport(videoCodec) : false // 检查音频编解码器兼容性 - const audioSupported = this.checkAudioCodecSupport(audioCodec) + const audioSupported = audioCodec ? this.checkAudioCodecSupport(audioCodec) : false // 确定是否需要转码 const needsTranscode = !videoSupported || !audioSupported // 生成不兼容原因列表 const incompatibilityReasons: string[] = [] - if (!videoSupported) { + if (!videoCodec) { + incompatibilityReasons.push('video-codec-missing') + } else if (!videoSupported) { incompatibilityReasons.push(`video-codec-unsupported: ${videoCodec}`) } - if (!audioSupported) { + if (!audioCodec) { + incompatibilityReasons.push('audio-codec-missing') + } else if (!audioSupported) { incompatibilityReasons.push(`audio-codec-unsupported: ${audioCodec}`) } From a21b99549c175bf7b980e2ae8ffd3495070daf6c Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 21:00:55 +0800 Subject: [PATCH 06/18] fix(UvBootstrapperService): ensure temp directory cleanup after download - Move the creation of target and temp directories outside of the try block for better scope management. - Ensure the cleanup of temporary directories in the finally block to prevent leftover files after download completion or failure. - Improve resource management by guaranteeing temp files are removed regardless of download success. --- src/main/services/UvBootstrapperService.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/services/UvBootstrapperService.ts b/src/main/services/UvBootstrapperService.ts index 1e0bbed6..5d9a7f71 100644 --- a/src/main/services/UvBootstrapperService.ts +++ b/src/main/services/UvBootstrapperService.ts @@ -466,12 +466,12 @@ export class UvBootstrapperService { this.downloadProgress.set(key, progress) + const platformDir = `${version.version}-${platform}-${arch}` + const targetDir = path.join(this.binariesDir, platformDir) + const tempDir = path.join(this.binariesDir, '.temp', key) + try { // 创建目标目录 - const platformDir = `${version.version}-${platform}-${arch}` - const targetDir = path.join(this.binariesDir, platformDir) - const tempDir = path.join(this.binariesDir, '.temp', key) - this.ensureDir(targetDir) this.ensureDir(tempDir) @@ -550,9 +550,6 @@ export class UvBootstrapperService { logger.info('uv 下载完成', { platform, arch, finalPath }) - // 清理临时文件 - this.cleanupTempDir(tempDir) - // 清除缓存以便重新检测 UvBootstrapperService.clearUvCache(platform, arch) @@ -571,6 +568,7 @@ export class UvBootstrapperService { } finally { this.downloadProgress.delete(key) this.downloadController.delete(key) + this.cleanupTempDir(tempDir) } } From 13c6b7eb39cabcc18af5f7165573a63805991854 Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 21:07:02 +0800 Subject: [PATCH 07/18] fix(VolumeIndicator): skip indicator display on initial render - Add useRef to track initial mount state - Prevent volume indicator from showing on component's first render - Ensure indicator only appears on subsequent volume changes This change enhances user experience by avoiding unnecessary indicator display during initial component load. --- .../src/pages/player/components/VolumeIndicator.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/pages/player/components/VolumeIndicator.tsx b/src/renderer/src/pages/player/components/VolumeIndicator.tsx index e80ccca6..22810069 100644 --- a/src/renderer/src/pages/player/components/VolumeIndicator.tsx +++ b/src/renderer/src/pages/player/components/VolumeIndicator.tsx @@ -1,5 +1,5 @@ import { usePlayerStore } from '@renderer/state/stores/player.store' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import VideoStatusIndicator from './VideoStatusIndicator' @@ -13,9 +13,16 @@ function VolumeIndicator() { const muted = usePlayerStore((s) => s.muted) const [showIndicator, setShowIndicator] = useState(false) + const isInitialMount = useRef(true) // 监听音量变化,显示指示器并在 1 秒后自动隐藏 useEffect(() => { + // 跳过初次渲染 + if (isInitialMount.current) { + isInitialMount.current = false + return + } + setShowIndicator(true) const timer = setTimeout(() => { From 08b96eb185f9c749574161384aa74785874b1077 Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 21:07:35 +0800 Subject: [PATCH 08/18] fix(UvBootstrapperService): enhance UV download logic with cached path checks - Add logic to check for cached binary paths before initiating downloads - Ensure system checks are only performed when platform and architecture match the current process - Improve logging to reflect when UV binaries are already present and downloads are skipped - Refactor download logic to optimize performance and reduce unnecessary downloads This update optimizes the UV download process by leveraging cached paths and system checks, ensuring efficient resource utilization and improved performance. --- src/main/services/UvBootstrapperService.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/services/UvBootstrapperService.ts b/src/main/services/UvBootstrapperService.ts index 5d9a7f71..ea92ec43 100644 --- a/src/main/services/UvBootstrapperService.ts +++ b/src/main/services/UvBootstrapperService.ts @@ -413,13 +413,22 @@ export class UvBootstrapperService { ): Promise { const key = `${platform}-${arch}` - // 检查是否已存在 - const installation = await this.checkUvInstallation() - if (installation.exists && installation.isDownloaded) { - logger.info('uv 已存在,跳过下载', { platform, arch, path: installation.path }) + // 检查目标平台的缓存二进制 + const downloadedPath = this.getDownloadedUvPath(platform, arch) + if (downloadedPath) { + logger.info('uv 已存在,跳过下载', { platform, arch, path: downloadedPath }) return true } + // 仅当请求的平台与当前进程一致时,才额外使用缓存的系统检测结果 + if (platform === process.platform && arch === process.arch) { + const installation = await this.checkUvInstallation() + if (installation.exists && installation.isDownloaded) { + logger.info('uv 已存在,跳过下载', { platform, arch, path: installation.path }) + return true + } + } + // 检查是否正在下载 if (this.downloadProgress.has(key)) { logger.warn('uv 正在下载中', { platform, arch }) From 382f6bf8a18d16b61c1dcaa22ce316eef9585d78 Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 21:09:09 +0800 Subject: [PATCH 09/18] fix(FFprobeSection): ensure timeout cleanup after download success - Add cleanup for success timeout to prevent potential memory leaks - Ensure proper handling of download completion state transitions This fix addresses the issue of unhandled timeouts after FFprobe download success, ensuring resources are properly managed. --- src/renderer/src/pages/settings/FFprobeSection.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/pages/settings/FFprobeSection.tsx b/src/renderer/src/pages/settings/FFprobeSection.tsx index 0aa8fb47..aa35a07e 100644 --- a/src/renderer/src/pages/settings/FFprobeSection.tsx +++ b/src/renderer/src/pages/settings/FFprobeSection.tsx @@ -117,13 +117,16 @@ const FFprobeSection: FC = ({ refreshKey = 0 }) => { message.success(t('settings.plugins.ffprobe.download.success')) // 2秒后恢复正常状态 - setTimeout(() => { + const successTimeout = setTimeout(() => { setIsDownloading(false) setShowSuccessState(false) setFFprobeStatus(currentStatus) // 更新 FFprobe 路径为下载后的路径 setFFprobePath(currentStatus.path) }, 2000) + + // 清理定时器 + return () => clearTimeout(successTimeout) } } catch (error) { logger.error('获取下载进度失败:', { error }) From 90082f8791093e5202bb3e9ec79c3c8e131b6350 Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 21:12:04 +0800 Subject: [PATCH 10/18] fix(FFprobeSection): standardize spacing in styled components - Replace hardcoded gap values with theme-based spacing constants - Ensure consistent styling across components using SPACING.XS This change improves maintainability and consistency in the styling of the FFprobeSection component by utilizing predefined theme constants. --- src/renderer/src/pages/settings/FFmpegSection.tsx | 4 ++-- src/renderer/src/pages/settings/FFprobeSection.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/pages/settings/FFmpegSection.tsx b/src/renderer/src/pages/settings/FFmpegSection.tsx index 78fe120f..b389bcca 100644 --- a/src/renderer/src/pages/settings/FFmpegSection.tsx +++ b/src/renderer/src/pages/settings/FFmpegSection.tsx @@ -468,8 +468,8 @@ const FFmpegSection: FC = ({ refreshKey = 0 }) => { const StatusContainer = styled.div` display: flex; align-items: center; - gap: 8px; - font-size: 14px; + gap: ${SPACING.XS}px; + font-size: ${FONT_SIZES.SM}px; ` // 下载按钮容器 diff --git a/src/renderer/src/pages/settings/FFprobeSection.tsx b/src/renderer/src/pages/settings/FFprobeSection.tsx index aa35a07e..22d25b14 100644 --- a/src/renderer/src/pages/settings/FFprobeSection.tsx +++ b/src/renderer/src/pages/settings/FFprobeSection.tsx @@ -417,7 +417,7 @@ const FFprobeSection: FC = ({ refreshKey = 0 }) => { const StatusContainer = styled.div` display: flex; align-items: center; - gap: 8px; + gap: ${SPACING.XS}px; font-size: 14px; ` @@ -544,7 +544,7 @@ const CancelButton = styled(Button)` const PathInputContainer = styled.div` display: flex; - gap: 8px; + gap: ${SPACING.XS}px; align-items: center; .ant-input { From 2905bf6c29156dc3ca94230c6ecaddc3badf6ab9 Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 21:16:24 +0800 Subject: [PATCH 11/18] fix(FFmpegSection): manage completion timeout for download process - Add a ref to handle completion timeout for download success state - Clear existing timeout before setting a new one to prevent overlapping - Ensure timeout is cleared on component unmount to avoid memory leaks This update ensures that the download completion state is managed properly, preventing potential issues with overlapping timeouts and ensuring proper cleanup. --- src/renderer/src/pages/settings/FFmpegSection.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/pages/settings/FFmpegSection.tsx b/src/renderer/src/pages/settings/FFmpegSection.tsx index b389bcca..23aa2d40 100644 --- a/src/renderer/src/pages/settings/FFmpegSection.tsx +++ b/src/renderer/src/pages/settings/FFmpegSection.tsx @@ -73,6 +73,7 @@ const FFmpegSection: FC = ({ refreshKey = 0 }) => { const [showSuccessState, setShowSuccessState] = useState(false) const isCancellingRef = useRef(false) const isCompletionHandledRef = useRef(false) + const completionTimeoutRef = useRef(null) const [isValidatingPath, setIsValidatingPath] = useState(false) // 获取 FFmpeg 状态 @@ -149,7 +150,10 @@ const FFmpegSection: FC = ({ refreshKey = 0 }) => { message.success(t('settings.plugins.ffmpeg.download.success')) // 2秒后恢复正常状态 - setTimeout(() => { + if (completionTimeoutRef.current) { + clearTimeout(completionTimeoutRef.current) + } + completionTimeoutRef.current = setTimeout(() => { setIsDownloading(false) setShowSuccessState(false) setFFmpegStatus(currentStatus) @@ -157,6 +161,7 @@ const FFmpegSection: FC = ({ refreshKey = 0 }) => { setFFmpegPath(currentStatus.path) // 自动开始预热 handleWarmup() + completionTimeoutRef.current = null }, 2000) } } catch (error) { @@ -169,6 +174,10 @@ const FFmpegSection: FC = ({ refreshKey = 0 }) => { if (progressInterval) { clearInterval(progressInterval) } + if (completionTimeoutRef.current) { + clearTimeout(completionTimeoutRef.current) + completionTimeoutRef.current = null + } } }, [handleWarmup, isDownloading, t]) From 07f4f3d9ce8d31467cbd84a32deb9346813a91f2 Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 21:19:32 +0800 Subject: [PATCH 12/18] fix(TranscodeLoadingIndicator): remove logging for loading indicator display - Eliminate logger usage for state changes in TranscodeLoadingIndicator - Simplify component by removing unnecessary debug logs This change reduces the complexity of the component by removing logging that was not essential for the functionality of the loading indicator. --- .../player/components/TranscodeLoadingIndicator.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/renderer/src/pages/player/components/TranscodeLoadingIndicator.tsx b/src/renderer/src/pages/player/components/TranscodeLoadingIndicator.tsx index e929ff08..3167693b 100644 --- a/src/renderer/src/pages/player/components/TranscodeLoadingIndicator.tsx +++ b/src/renderer/src/pages/player/components/TranscodeLoadingIndicator.tsx @@ -1,11 +1,8 @@ -import { loggerService } from '@logger' import { usePlayerStore } from '@renderer/state/stores/player.store' import { useMemo } from 'react' import VideoStatusIndicator from './VideoStatusIndicator' -const logger = loggerService.withContext('TranscodeLoadingIndicator') - /** * 转码加载指示器组件 * 在用户 seek 到未转码时间点时显示加载动画 @@ -32,15 +29,6 @@ function TranscodeLoadingIndicator() { return false }, [hlsMode, transcodeStatus, isVideoSeeking, isVideoWaiting]) - // 日志记录状态变化 - if (showLoading) { - logger.debug('显示转码加载指示器', { - transcodeStatus, - isVideoSeeking, - isVideoWaiting - }) - } - return } From ca04424d4c34aef66c9df0e445b0c6b6c9467d1f Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 21:28:45 +0800 Subject: [PATCH 13/18] fix(FFprobeSection): add return statement to download progress polling function - Ensure proper cleanup by adding an empty return statement in the download progress polling function. - This change prevents potential issues with function execution flow and enhances code readability. --- src/renderer/src/pages/settings/FFprobeSection.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/src/pages/settings/FFprobeSection.tsx b/src/renderer/src/pages/settings/FFprobeSection.tsx index 22d25b14..14cd0a62 100644 --- a/src/renderer/src/pages/settings/FFprobeSection.tsx +++ b/src/renderer/src/pages/settings/FFprobeSection.tsx @@ -131,6 +131,7 @@ const FFprobeSection: FC = ({ refreshKey = 0 }) => { } catch (error) { logger.error('获取下载进度失败:', { error }) } + return () => {} }, 2000) } From 010059166a265b1246bc14f1c00bd4785bc8edc8 Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 21:46:18 +0800 Subject: [PATCH 14/18] fix(FFprobeSection): manage success timeout for download completion - Add successTimeoutRef to handle and clear timeout for success state transition - Ensure proper cleanup of success timeout on component unmount - Prevent multiple timeout instances by clearing existing timeout before setting a new one These changes improve the reliability of the download success state transition by ensuring timeouts are properly managed and cleaned up. --- .../src/pages/settings/FFprobeSection.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/pages/settings/FFprobeSection.tsx b/src/renderer/src/pages/settings/FFprobeSection.tsx index 14cd0a62..34588fc6 100644 --- a/src/renderer/src/pages/settings/FFprobeSection.tsx +++ b/src/renderer/src/pages/settings/FFprobeSection.tsx @@ -64,6 +64,7 @@ const FFprobeSection: FC = ({ refreshKey = 0 }) => { const [showSuccessState, setShowSuccessState] = useState(false) const isCancellingRef = useRef(false) const isCompletionHandledRef = useRef(false) + const successTimeoutRef = useRef(null) const [isValidatingPath, setIsValidatingPath] = useState(false) // 获取 FFprobe 状态 @@ -117,25 +118,29 @@ const FFprobeSection: FC = ({ refreshKey = 0 }) => { message.success(t('settings.plugins.ffprobe.download.success')) // 2秒后恢复正常状态 - const successTimeout = setTimeout(() => { + if (successTimeoutRef.current) { + clearTimeout(successTimeoutRef.current) + } + successTimeoutRef.current = window.setTimeout(() => { setIsDownloading(false) setShowSuccessState(false) setFFprobeStatus(currentStatus) // 更新 FFprobe 路径为下载后的路径 setFFprobePath(currentStatus.path) + successTimeoutRef.current = null }, 2000) - - // 清理定时器 - return () => clearTimeout(successTimeout) } } catch (error) { logger.error('获取下载进度失败:', { error }) } - return () => {} }, 2000) } return () => { + if (successTimeoutRef.current) { + clearTimeout(successTimeoutRef.current) + successTimeoutRef.current = null + } if (progressInterval) { clearInterval(progressInterval) } From 0303acb01678da816bd6b1d4cc9974048cb0049b Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 21:56:45 +0800 Subject: [PATCH 15/18] fix(FFprobeSection): standardize font size using theme constants - Update font size in StatusContainer to use FONT_SIZES.SM from theme constants for consistency across the application. This change ensures uniformity in font sizing, improving the visual coherence of the UI. --- src/renderer/src/pages/settings/FFprobeSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/pages/settings/FFprobeSection.tsx b/src/renderer/src/pages/settings/FFprobeSection.tsx index 34588fc6..23d457c3 100644 --- a/src/renderer/src/pages/settings/FFprobeSection.tsx +++ b/src/renderer/src/pages/settings/FFprobeSection.tsx @@ -424,7 +424,7 @@ const StatusContainer = styled.div` display: flex; align-items: center; gap: ${SPACING.XS}px; - font-size: 14px; + font-size: ${FONT_SIZES.SM}px; ` // 下载按钮容器 From 290cabeed406861842ab65496d0ee9470113e642 Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 22:03:41 +0800 Subject: [PATCH 16/18] fix(MediaServerService): replace fs.existsSync with async stat for file existence check - Remove synchronous fs.existsSync checks and replace them with asynchronous stat calls to improve non-blocking behavior. - Simplify logic in removeDirectoryIfExists by directly attempting to remove directories without prior existence check. - Enhance error handling for file existence validation in cleanupCachesForFile method. --- src/main/services/MediaServerService.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/services/MediaServerService.ts b/src/main/services/MediaServerService.ts index cde0bb7f..2656639f 100644 --- a/src/main/services/MediaServerService.ts +++ b/src/main/services/MediaServerService.ts @@ -1,5 +1,4 @@ import { createHash } from 'node:crypto' -import fs from 'node:fs' import { open, rm, stat } from 'node:fs/promises' import { IpcChannel } from '@shared/IpcChannel' @@ -537,7 +536,9 @@ export class MediaServerService { */ public async cleanupCachesForFile(filePath: string): Promise { const resolvedPath = path.resolve(filePath) - if (!fs.existsSync(resolvedPath)) { + try { + await stat(resolvedPath) + } catch { throw new Error(`文件不存在: ${resolvedPath}`) } @@ -608,10 +609,6 @@ export class MediaServerService { dirPath: string ): Promise<{ removed: boolean; error?: string }> { try { - if (!fs.existsSync(dirPath)) { - return { removed: false } - } - await rm(dirPath, { recursive: true, force: true }) logger.info('已删除转码缓存目录', { dirPath }) return { removed: true } From ef08d35d82c4e70f002c1350b8e263f0cfc4117e Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 22:05:23 +0800 Subject: [PATCH 17/18] fix(UvBootstrapperService): prevent concurrent downloads by checking download controllers - Add condition to check for existing download controllers to avoid initiating multiple downloads for the same platform and architecture. - Improve logging to warn when a download is already in progress. --- src/main/services/UvBootstrapperService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/services/UvBootstrapperService.ts b/src/main/services/UvBootstrapperService.ts index ea92ec43..f728732b 100644 --- a/src/main/services/UvBootstrapperService.ts +++ b/src/main/services/UvBootstrapperService.ts @@ -430,7 +430,7 @@ export class UvBootstrapperService { } // 检查是否正在下载 - if (this.downloadProgress.has(key)) { + if (this.downloadProgress.has(key) || this.downloadController.has(key)) { logger.warn('uv 正在下载中', { platform, arch }) return false } From 1cee7913bb63638e1ab914b97243067b49f02847 Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Mon, 13 Oct 2025 22:23:10 +0800 Subject: [PATCH 18/18] fix(MediaServerService): enhance error handling for file existence check - Add specific error handling for ENOENT code in cleanupCachesForFile method - Ensure other errors are propagated correctly --- src/main/services/MediaServerService.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/services/MediaServerService.ts b/src/main/services/MediaServerService.ts index 2656639f..56539b73 100644 --- a/src/main/services/MediaServerService.ts +++ b/src/main/services/MediaServerService.ts @@ -538,8 +538,12 @@ export class MediaServerService { const resolvedPath = path.resolve(filePath) try { await stat(resolvedPath) - } catch { - throw new Error(`文件不存在: ${resolvedPath}`) + } catch (err) { + const code = (err as NodeJS.ErrnoException).code + if (code === 'ENOENT') { + throw new Error(`文件不存在: ${resolvedPath}`) + } + throw err } const assetHash = await this.calculateAssetHash(resolvedPath)