Skip to content

feat(media-server): implement runtime runtime management system#204

Merged
mkdir700 merged 11 commits intodevfrom
feat/media-server-runtime
Oct 12, 2025
Merged

feat(media-server): implement runtime runtime management system#204
mkdir700 merged 11 commits intodevfrom
feat/media-server-runtime

Conversation

@mkdir700
Copy link
Owner

@mkdir700 mkdir700 commented Oct 11, 2025

  • add UV bootstrapper with cross-platform install, caching, progress, validation

  • manage Python venv lifecycle, dependency installs, cleanup, and phased progress

  • introduce MediaServerService with lifecycle control, health checks, and port management

  • extend FFmpeg service to handle FFprobe with unified downloads and version updates

  • build settings UI sections for media server and plugins with real-time status

  • wire up 38 IPC channels covering UV, venv, FFmpeg/FFprobe, and server control

  • update build configs, add ffprobe download script, auto-start server, and bump backend ref

Summary by CodeRabbit

  • 新功能

    • 完整新增 FFprobe 管理(检测/下载/进度/取消/卸载)并补充中文文案与设置页组件
    • 引入 UV 引导器、Python 虚拟环境管理与媒体服务器(安装/启动/停止/重启/端口通知/自动启动),并在设置页整合为插件中心
    • 预加载 API 扩展:ffprobe、uv、pythonVenv、mediaServer;播放器改为动态获取播放地址
  • 杂务

    • 打包改为白名单过滤以缩小范围
    • .gitignore 新增 ffprobe 资源与缓存条目
    • 更新 FFmpeg/FFprobe 下载逻辑(macOS FFmpeg 升级至 8.0)并新增下载脚本
  • 测试

    • 新增 UvBootstrapperService 测试并更新/扩展与下载相关测试集

- add UV bootstrapper with cross-platform install, caching, progress, validation

- manage Python venv lifecycle, dependency installs, cleanup, and phased progress

- introduce MediaServerService with lifecycle control, health checks, and port management

- extend FFmpeg service to handle FFprobe with unified downloads and version updates

- build settings UI sections for media server and plugins with real-time status

- wire up 38 IPC channels covering UV, venv, FFmpeg/FFprobe, and server control

- update build configs, add ffprobe download script, auto-start server, and bump backend ref
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 11, 2025

Walkthrough

新增 FFprobe 下载脚本与服务、UV 引导器、Python 虚拟环境与媒体服务器服务;将 FFmpeg 下载服务泛化以支持 ffprobe;扩展 IPC 与 preload API;更新打包/复制规则、SessionService 动态端口及播放器播放列表获取;新增设置页与大量单元测试。

Changes

Cohort / File(s) Summary
版本控制与子模块
\.gitignore, backend
.gitignore 新增 resources/ffprobe/.ffprobe-cachebackend 子模块指针更新。
打包/构建配置
electron-builder.yml, electron.vite.config.ts
打包从广泛排除改为显式 include 过滤;Vite 插件 copy-media-server 改为基于 fs.Dirent 的排除策略(目录 regex、文件名集合、后缀集合),并修改了 shouldExclude 签名与遍历实现。
下载脚本
scripts/download-ffmpeg.ts, scripts/download-ffprobe.ts
FFmpeg macOS 包改为 v8.0;新增完整 scripts/download-ffprobe.ts(多平台配置、缓存、下载、解压、权限、进度、CLI、导出 FFprobeDownloader)。
共享类型与 IPC 枚举
packages/shared/types/*.ts, packages/shared/IpcChannel.ts
新增共享类型 Platform/Arch 与 media-server 类型;扩展 IpcChannel 枚举,加入 FFprobe/FFprobeDownload/UV/PythonVenv/MediaServer 等通道。
主进程下载/服务
src/main/services/FFmpegDownloadService.ts, src/main/services/UvBootstrapperService.ts, src/main/services/PythonVenvService.ts, src/main/services/MediaServerService.ts
将 FFmpegDownloadService 泛化为支持 binaryTypeffmpeg/ffprobe)并新增对应 API(路径、版本、下载、移除、进度、取消);新增 UvBootstrapperService(uv 发现/下载/镜像测速/执行);新增 PythonVenvService(venv 管理、依赖安装、进度);新增 MediaServerService(端口分配、env 构建、spawn、健康检查、重启/停止、通知)。
主进程启动与 IPC 注册
src/main/index.ts, src/main/ipc.ts
启动流程新增基于 python venv 状态的媒体服务器自动启动尝试;IPC 注册扩展以支持 FFprobe/UV/PythonVenv/MediaServer 的查询与管理处理器。
预加载层 API
src/preload/index.ts
preload 暴露 ffprobeuvpythonVenvmediaServer API,对应新增 IPC 通道。
渲染端、会话与播放器
src/renderer/src/services/SessionService.ts, src/renderer/src/pages/player/PlayerPage.tsx
SessionService 改为基于媒体服务器动态端口构建 API 基址,新增 getPlaylistUrlresetPort 并监听端口变更;PlayerPage 异步获取播放列表 URL 作为 HLS 源。
设置页与本地化
src/renderer/src/pages/settings/*.tsx, src/renderer/src/i18n/locales/zh-cn.json
新增 FFprobe 与 Media Server 设置面板,重构/新增 FFmpeg 设置组件,新增 PluginsSettings 并替换路由;添加中文本地化 plugins.ffprobe 条目。
测试与 mocks
src/main/services/__tests__/*, src/main/__tests__/ipc.database.test.ts
更新 FFmpeg 测试以匹配新版签名与版本;新增 UvBootstrapperService 广泛单元测试;Electron app mock 扩展并统一用于 IPC 测试。

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App as 主进程
  participant Venv as PythonVenvService
  participant MS as MediaServerService
  participant UV as UvBootstrapperService

  App->>Venv: checkVenvInfo()
  alt venv 存在
    App->>MS: start(config?)
    MS->>UV: checkUvInstallation()
    UV-->>MS: {exists, path, version}
    MS->>MS: 分配端口 / 构建 env / spawn uvicorn
    MS-->>App: 启动结果(成功/失败)
  else venv 不存在
    App-->>App: 跳过媒体服务器启动
  end
Loading
sequenceDiagram
  autonumber
  participant UI as Renderer(设置页)
  participant Pre as Preload(ffprobe)
  participant IPC as Main IPC
  participant Svc as FFmpegDownloadService

  UI->>Pre: ffprobe.download.download(platform, arch)
  Pre->>IPC: IpcChannel.FfprobeDownload_Download
  IPC->>Svc: downloadFFprobe(platform, arch)
  Svc-->>IPC: 进度/结果事件
  IPC-->>Pre: boolean
  loop 轮询
    UI->>Pre: ffprobe.download.getProgress()
    Pre->>IPC: IpcChannel.FfprobeDownload_GetProgress
    IPC-->>UI: {percent,speed,remainingTime,status}
  end
Loading
sequenceDiagram
  autonumber
  participant Player as Renderer(PlayerPage)
  participant SS as SessionService
  participant Pre as Preload(mediaServer)
  participant IPC as Main IPC
  participant MS as MediaServerService

  Player->>SS: getPlaylistUrl(sessionId)
  alt port 未缓存
    SS->>Pre: mediaServer.getPort()
    Pre->>IPC: IpcChannel.MediaServer_GetPort
    IPC->>MS: getPort()
    MS-->>IPC: port|null
    IPC-->>Pre: port|null
    Pre-->>SS: port|null
    SS->>Player: 构建并返回 playlist URL
  else 已缓存
    SS-->>Player: playlist URL
  end
  note over SS,Player: 收到 MediaServer_PortChanged 时调用 resetPort()
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Possibly related PRs

Poem

我是小兔在仓库忙,
下载 FFprobe 咚咚响,
UV 引路风轻扬,
Venv 点火服务器忙,
播放器欢跃声朗朗 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title Check ⚠️ Warning PR 标题包含重复单词“runtime runtime”,降低了可读性且显得多余,应保持简洁明确地表达主要变更内容。 建议将标题修改为“feat(media-server): implement runtime management system”或“feat(media-server): implement media server runtime management system”,去除重复并保持简洁清晰。
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/media-server-runtime

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
src/main/ipc.ts (1)

508-515: 修复 FFmpeg 下载进度/取消的参数顺序

FFmpegDownloadService 现在要求第一个参数是 'ffmpeg' | 'ffprobe' 来区分二进制类型(测试文件也已经改为 getDownloadProgress('ffmpeg', ...) / cancelDownload('ffmpeg', ...))。这里仍然用旧签名,把 platform 传成首参,实际会查不到 'win32' 这种 key,导致进度始终为 null、取消操作无效。请同步补上 binaryType。

建议修改如下:

-      return ffmpegDownloadService.getDownloadProgress(platform as any, arch as any)
+      return ffmpegDownloadService.getDownloadProgress('ffmpeg', platform as any, arch as any)
 ...
-    return ffmpegDownloadService.cancelDownload(platform as any, arch as any)
+    return ffmpegDownloadService.cancelDownload('ffmpeg', platform as any, arch as any)
src/main/services/FFmpegDownloadService.ts (6)

12-15: 类型重复定义,建议复用单一来源

Platform/Arch 在 UvBootstrapperService 已导出,这里重复定义会造成漂移风险。建议直接 import type 统一来源。

-// 支持的平台类型
-export type Platform = 'win32' | 'darwin' | 'linux'
-export type Arch = 'x64' | 'arm64'
+// 复用统一类型定义
+import type { Platform, Arch } from './UvBootstrapperService'

879-885: 仅处理 301/302 重定向,遗漏 303/307/308

实际下载源(GitHub、第三方镜像)可能返回 303/307/308。建议一并处理。

-            if (response.statusCode === 301 || response.statusCode === 302) {
+            if ([301, 302, 303, 307, 308].includes(response.statusCode ?? 0)) {
               const redirectUrl = response.headers.location
               if (redirectUrl) {
                 download(redirectUrl, redirectCount + 1)
                 return
               }
             }

939-942: 请求超时处理更稳妥的写法

https.get 的 options.timeout 兼容性不如 req.setTimeout 明确。建议改为 setTimeout。

-        request.on('timeout', () => {
-          request.destroy()
-          reject(new Error('下载超时'))
-        })
+        request.setTimeout(30_000, () => {
+          request.destroy(new Error('下载超时'))
+          reject(new Error('下载超时'))
+        })

964-992: ZIP 解压依赖系统工具,Linux/某些发行版可能缺少 unzip

当 unzip 不存在会 ENOENT 失败。建议增加 JS 解压回退(如 yauzl/adm-zip),或在失败时提示安装依赖。

可在捕获 ENOENT 时回退到 JS 解压:

  • 优先使用系统工具(当前逻辑)
  • 捕获 error.code === 'ENOENT' → 动态 import 解压库并执行

303-319: 地区检测失败默认切到中国镜像,需确认是否符合产品预期

检测失败时 useChinaMirror = true,可能导致全球用户先走中国镜像再回退,增加失败/时延。请确认策略;若面向全球,建议默认 global。

候选策略:

  • 默认 global;仅在 country ∈ {cn,hk,mo,tw} 时启用 china
  • 或增加用户可见设置,允许覆盖

618-722: 下载/解压失败后的残留及幂等性

失败时可能残留部分下载文件或不完整目录。可考虑:

  • 失败后清理临时目录与目标文件(若已创建)
  • 写入时先到临时文件,再原子替换到 finalPath
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cbe6047 and 56c26f6.

📒 Files selected for processing (24)
  • .gitignore (1 hunks)
  • backend (1 hunks)
  • electron-builder.yml (1 hunks)
  • electron.vite.config.ts (2 hunks)
  • packages/shared/IpcChannel.ts (1 hunks)
  • scripts/download-ffmpeg.ts (1 hunks)
  • scripts/download-ffprobe.ts (1 hunks)
  • src/main/index.ts (2 hunks)
  • src/main/ipc.ts (2 hunks)
  • src/main/services/FFmpegDownloadService.ts (22 hunks)
  • src/main/services/MediaServerService.ts (1 hunks)
  • src/main/services/PythonVenvService.ts (1 hunks)
  • src/main/services/UvBootstrapperService.ts (1 hunks)
  • src/main/services/__tests__/FFmpegDownloadService.test.ts (3 hunks)
  • src/main/services/__tests__/UvBootstrapperService.test.ts (1 hunks)
  • src/preload/index.ts (2 hunks)
  • src/renderer/src/i18n/locales/zh-cn.json (1 hunks)
  • src/renderer/src/pages/player/PlayerPage.tsx (1 hunks)
  • src/renderer/src/pages/settings/FFmpegSection.tsx (1 hunks)
  • src/renderer/src/pages/settings/FFprobeSection.tsx (1 hunks)
  • src/renderer/src/pages/settings/MediaServerSection.tsx (1 hunks)
  • src/renderer/src/pages/settings/PluginsSettings.tsx (1 hunks)
  • src/renderer/src/pages/settings/SettingsPage.tsx (2 hunks)
  • src/renderer/src/services/SessionService.ts (5 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
src/renderer/src/**/*.{ts,tsx,scss,css}

📄 CodeRabbit inference engine (CLAUDE.md)

优先使用 CSS 变量,避免硬编码样式值(颜色等)

Files:

  • src/renderer/src/pages/player/PlayerPage.tsx
  • src/renderer/src/pages/settings/FFprobeSection.tsx
  • src/renderer/src/services/SessionService.ts
  • src/renderer/src/pages/settings/PluginsSettings.tsx
  • src/renderer/src/pages/settings/SettingsPage.tsx
  • src/renderer/src/pages/settings/FFmpegSection.tsx
  • src/renderer/src/pages/settings/MediaServerSection.tsx
src/renderer/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

尺寸与时长等不要硬编码,优先使用 useTheme() 的 token 或集中样式变量(如 motionDurationMid、borderRadiusSM/MD)

Files:

  • src/renderer/src/pages/player/PlayerPage.tsx
  • src/renderer/src/pages/settings/FFprobeSection.tsx
  • src/renderer/src/services/SessionService.ts
  • src/renderer/src/pages/settings/PluginsSettings.tsx
  • src/renderer/src/pages/settings/SettingsPage.tsx
  • src/renderer/src/pages/settings/FFmpegSection.tsx
  • src/renderer/src/pages/settings/MediaServerSection.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: 定制 antd 组件样式优先使用 styled-components 包装 styled(Component),避免全局 classNames
项目的图标统一使用 lucide-react,不使用 emoji 作为图标
组件/Hook 顶层必须通过 useStore(selector) 使用 Zustand,禁止在 useMemo/useEffect 内部调用 store Hook
避免使用返回对象的 Zustand 选择器(如 useStore(s => ({ a: s.a, b: s.b })));应使用单字段选择器或配合 shallow 比较器
遵循 React「副作用与状态更新」规范:渲染纯函数、Effect 三分法、幂等更新、稳定引用、严格清理、禁止写回自身依赖、Provider 值 memo、外部状态 selector 稳定等
统一使用 loggerService 记录日志而不是 console
logger 使用示例中第二个参数必须为对象字面量(如 logger.error('msg', { error }))
任何组件或页面都不要写入 media 元素的 currentTime,播放器控制由编排器统一负责
在 styled-components 中:主题相关属性使用 AntD CSS 变量(如 var(--ant-color-bg-elevated));
在 styled-components 中:设计系统常量(尺寸、动画、层级、字体、毛玻璃等)使用 JS 常量(如 SPACING、BORDER_RADIUS、Z_INDEX、FONT_SIZES 等)

Files:

  • src/renderer/src/pages/player/PlayerPage.tsx
  • scripts/download-ffmpeg.ts
  • src/main/services/__tests__/FFmpegDownloadService.test.ts
  • src/renderer/src/pages/settings/FFprobeSection.tsx
  • src/renderer/src/services/SessionService.ts
  • src/renderer/src/pages/settings/PluginsSettings.tsx
  • src/renderer/src/pages/settings/SettingsPage.tsx
  • src/main/services/PythonVenvService.ts
  • src/renderer/src/pages/settings/FFmpegSection.tsx
  • src/main/services/MediaServerService.ts
  • scripts/download-ffprobe.ts
  • src/main/services/UvBootstrapperService.ts
  • src/main/ipc.ts
  • src/preload/index.ts
  • packages/shared/IpcChannel.ts
  • src/main/services/__tests__/UvBootstrapperService.test.ts
  • src/renderer/src/pages/settings/MediaServerSection.tsx
  • electron.vite.config.ts
  • src/main/index.ts
  • src/main/services/FFmpegDownloadService.ts
**/player/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Player 页面:统一在组件顶层使用 Zustand selector,禁止在 useMemo/useEffect 内调用 store Hook;useSubtitleEngine 通过参数传入 subtitles 等防御处理

Files:

  • src/renderer/src/pages/player/PlayerPage.tsx
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

使用 Vitest 作为测试框架

Files:

  • src/main/services/__tests__/FFmpegDownloadService.test.ts
  • src/main/services/__tests__/UvBootstrapperService.test.ts
🧬 Code graph analysis (14)
src/renderer/src/pages/player/PlayerPage.tsx (1)
src/renderer/src/services/SessionService.ts (1)
  • SessionService (123-533)
src/renderer/src/pages/settings/FFprobeSection.tsx (4)
src/renderer/src/services/Logger.ts (2)
  • loggerService (817-817)
  • error (422-424)
src/renderer/src/contexts/theme.context.tsx (1)
  • useTheme (124-124)
src/renderer/src/pages/settings/AboutSettings.tsx (1)
  • SettingRowTitle (412-424)
src/renderer/src/infrastructure/styles/theme.ts (6)
  • SPACING (43-58)
  • BORDER_RADIUS (61-72)
  • ANIMATION_DURATION (91-100)
  • EASING (103-114)
  • FONT_WEIGHTS (11-22)
  • FONT_SIZES (25-40)
src/renderer/src/services/SessionService.ts (1)
src/renderer/src/services/Logger.ts (2)
  • loggerService (817-817)
  • error (422-424)
src/renderer/src/pages/settings/PluginsSettings.tsx (1)
src/renderer/src/contexts/theme.context.tsx (1)
  • useTheme (124-124)
src/main/services/PythonVenvService.ts (2)
src/renderer/src/services/Logger.ts (3)
  • loggerService (817-817)
  • error (422-424)
  • info (436-438)
src/main/services/UvBootstrapperService.ts (1)
  • uvBootstrapperService (1188-1188)
src/renderer/src/pages/settings/FFmpegSection.tsx (4)
src/renderer/src/services/Logger.ts (1)
  • loggerService (817-817)
src/renderer/src/contexts/theme.context.tsx (1)
  • useTheme (124-124)
src/renderer/src/pages/settings/AboutSettings.tsx (1)
  • SettingRowTitle (412-424)
src/renderer/src/infrastructure/styles/theme.ts (6)
  • SPACING (43-58)
  • BORDER_RADIUS (61-72)
  • ANIMATION_DURATION (91-100)
  • EASING (103-114)
  • FONT_WEIGHTS (11-22)
  • FONT_SIZES (25-40)
src/main/services/MediaServerService.ts (4)
src/main/services/FFmpegDownloadService.ts (1)
  • FFmpegDownloadService (280-1050)
src/main/utils/index.ts (1)
  • getDataPath (11-17)
src/main/services/UvBootstrapperService.ts (1)
  • uvBootstrapperService (1188-1188)
src/main/services/PythonVenvService.ts (1)
  • pythonVenvService (518-518)
src/main/services/UvBootstrapperService.ts (2)
src/renderer/src/services/Logger.ts (3)
  • loggerService (817-817)
  • error (422-424)
  • info (436-438)
src/main/services/FFmpegDownloadService.ts (3)
  • Platform (13-13)
  • Arch (14-14)
  • DownloadProgress (28-35)
src/main/ipc.ts (4)
src/main/services/FFmpegDownloadService.ts (1)
  • ffmpegDownloadService (1053-1053)
src/main/services/UvBootstrapperService.ts (1)
  • uvBootstrapperService (1188-1188)
src/main/services/PythonVenvService.ts (1)
  • pythonVenvService (518-518)
src/main/services/MediaServerService.ts (1)
  • mediaServerService (616-616)
src/main/services/__tests__/UvBootstrapperService.test.ts (2)
src/main/services/UvBootstrapperService.ts (2)
  • UvBootstrapperService (138-1185)
  • DownloadProgress (26-33)
src/main/services/FFmpegDownloadService.ts (1)
  • DownloadProgress (28-35)
src/renderer/src/pages/settings/MediaServerSection.tsx (6)
src/renderer/src/services/Logger.ts (3)
  • loggerService (817-817)
  • info (436-438)
  • error (422-424)
src/main/services/MediaServerService.ts (1)
  • MediaServerInfo (29-36)
src/main/services/PythonVenvService.ts (1)
  • InstallProgress (25-29)
src/renderer/src/contexts/theme.context.tsx (1)
  • useTheme (124-124)
src/renderer/src/pages/settings/AboutSettings.tsx (1)
  • SettingRowTitle (412-424)
src/renderer/src/infrastructure/styles/theme.ts (6)
  • SPACING (43-58)
  • BORDER_RADIUS (61-72)
  • ANIMATION_DURATION (91-100)
  • EASING (103-114)
  • FONT_WEIGHTS (11-22)
  • FONT_SIZES (25-40)
electron.vite.config.ts (1)
scripts/upload-assets.js (1)
  • fs (4-4)
src/main/index.ts (2)
src/main/services/PythonVenvService.ts (1)
  • pythonVenvService (518-518)
src/main/services/MediaServerService.ts (1)
  • mediaServerService (616-616)
src/main/services/FFmpegDownloadService.ts (1)
src/main/services/UvBootstrapperService.ts (3)
  • Platform (11-11)
  • Arch (12-12)
  • DownloadProgress (26-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test (macos-latest, 20)
  • GitHub Check: test (windows-latest, 20)
  • GitHub Check: test (ubuntu-latest, 20)
🔇 Additional comments (3)
src/renderer/src/pages/player/PlayerPage.tsx (1)

179-180: 动态 URL 构建:集中化维护

  • 将硬编码播放列表 URL 替换为 SessionService.getPlaylistUrl() 动态获取,✅ 异步调用和错误处理(try-catch)已覆盖
  • 建议确认媒体服务器自动启动逻辑在播放器加载前已完成,以避免 getPort() 返回 null
backend (1)

1-1: 请确认子模块更新内容已通过审查

此处仅更新了子模块指针,我在当前 PR 中无法看到新提交的代码,请确认指向的 d747012bbd0a394f644939e9c15212ea769274ab 已经过审核和验证,避免引入未预期的改动。

src/main/services/PythonVenvService.ts (1)

5-6: 无需修改 LoggerService 引入src/main/services/LoggerService.ts 存在且已实现 errorwarninfodebug 方法。

Comment on lines +106 to +162
// FFprobe 配置(与 FFmpeg 使用相同的包,只是提取不同的可执行文件)
const FFPROBE_VERSIONS: Record<Platform, Record<Arch, FFmpegVersion>> = {
win32: {
x64: {
version: 'latest',
platform: 'win32',
arch: 'x64',
url: 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip',
size: 89 * 1024 * 1024,
extractPath: 'ffmpeg-master-latest-win64-gpl/bin/ffprobe.exe'
},
arm64: {
version: 'latest',
platform: 'win32',
arch: 'arm64',
url: 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-winarm64-gpl.zip',
size: 85 * 1024 * 1024,
extractPath: 'ffmpeg-master-latest-winarm64-gpl/bin/ffprobe.exe'
}
},
darwin: {
x64: {
version: 'latest',
platform: 'darwin',
arch: 'x64',
url: 'https://evermeet.cx/ffprobe/ffprobe-8.0.zip',
size: 65 * 1024 * 1024,
extractPath: 'ffprobe'
},
arm64: {
version: 'latest',
platform: 'darwin',
arch: 'arm64',
url: 'https://evermeet.cx/ffprobe/ffprobe-8.0.zip',
size: 65 * 1024 * 1024,
extractPath: 'ffprobe'
}
},
linux: {
x64: {
version: 'latest',
platform: 'linux',
arch: 'x64',
url: 'https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz',
size: 35 * 1024 * 1024,
extractPath: 'ffmpeg-*-amd64-static/ffprobe'
},
arm64: {
version: 'latest',
platform: 'linux',
arch: 'arm64',
url: 'https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz',
size: 33 * 1024 * 1024,
extractPath: 'ffmpeg-*-arm64-static/ffprobe'
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

版本与下载源未做校验,建议补充 SHA256 校验或固定版本

当前使用 latest 与第三方源,无哈希校验。存在供应链与漂移风险。

  • 为各版本配置 sha256,并在下载后计算校验(已留 TODO,可实现)
  • 生产环境尽量固定具体版本而非 latest,必要时定期更新

Comment on lines +176 to +189
if (venvExists) {
try {
// 使用 uv 获取 Python 版本
const uvPath = await uvBootstrapperService.getAvailableUvPath()
if (uvPath) {
const result = await this.executeCommand(
uvPath,
['run', 'python', '--version'],
mediaServerPath
)
const versionMatch = result.match(/Python (\S+)/)
pythonVersion = versionMatch ? versionMatch[1] : undefined
}
} catch (error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

优先直接调用 venv 内的 python 获取版本,避免依赖 uv

当前通过 uv run python --version 获取版本。当 uv 遇到异常或未安装时会失败。既然已检测到 venvExists,也可直接调用 venv 的解释器更可靠。

-          const uvPath = await uvBootstrapperService.getAvailableUvPath()
-          if (uvPath) {
-            const result = await this.executeCommand(
-              uvPath,
-              ['run', 'python', '--version'],
-              mediaServerPath
-            )
-            const versionMatch = result.match(/Python (\S+)/)
-            pythonVersion = versionMatch ? versionMatch[1] : undefined
-          }
+          const result = await this.executeCommand(
+            pythonPath,
+            ['--version'],
+            mediaServerPath
+          )
+          const versionMatch = result.match(/Python (\S+)/)
+          pythonVersion = versionMatch ? versionMatch[1] : undefined
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (venvExists) {
try {
// 使用 uv 获取 Python 版本
const uvPath = await uvBootstrapperService.getAvailableUvPath()
if (uvPath) {
const result = await this.executeCommand(
uvPath,
['run', 'python', '--version'],
mediaServerPath
)
const versionMatch = result.match(/Python (\S+)/)
pythonVersion = versionMatch ? versionMatch[1] : undefined
}
} catch (error) {
if (venvExists) {
try {
// 使用 uv 获取 Python 版本
const result = await this.executeCommand(
pythonPath,
['--version'],
mediaServerPath
)
const versionMatch = result.match(/Python (\S+)/)
pythonVersion = versionMatch ? versionMatch[1] : undefined
} catch (error) {
🤖 Prompt for AI Agents
In src/main/services/PythonVenvService.ts around lines 176-189, the code
currently always tries to get the Python version via uv run, which can fail if
uv is absent; since venvExists is true, first invoke the venv's interpreter
directly to determine the version (resolve the venv Python path using
platform-specific locations: <venv>/bin/python on Unix and
<venv>/Scripts/python.exe on Windows), check that the file exists/executable,
run it with --version and parse the output into pythonVersion; only if the venv
interpreter is missing or calling it fails, fall back to using
uvBootstrapperService.getAvailableUvPath() and the existing uv run approach, and
ensure all calls are wrapped in try/catch so errors fall back cleanly without
throwing.

Comment on lines +331 to +334
if (pythonVersion) {
args.push('--python', pythonVersion)
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

校验 pythonVersion,阻止非法入参并提升健壮性

即便禁用 shell,仍建议对白名单格式进行校验,避免异常参数导致 uv 行为不可预期。

-      if (pythonVersion) {
-        args.push('--python', pythonVersion)
-      }
+      if (pythonVersion) {
+        // 仅允许形如 3、3.11、3.11.6 或 3.11.* 的版本标记
+        const isSafe = /^[0-9]+(\.[0-9]+){0,2}(\.\*)?$/.test(pythonVersion)
+        if (!isSafe) {
+          throw new Error(`非法的 Python 版本号: ${pythonVersion}`)
+        }
+        args.push('--python', pythonVersion)
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (pythonVersion) {
args.push('--python', pythonVersion)
}
if (pythonVersion) {
// 仅允许形如 3、3.11、3.11.6 或 3.11.* 的版本标记
const isSafe = /^[0-9]+(\.[0-9]+){0,2}(\.\*)?$/.test(pythonVersion)
if (!isSafe) {
throw new Error(`非法的 Python 版本号: ${pythonVersion}`)
}
args.push('--python', pythonVersion)
}
🤖 Prompt for AI Agents
In src/main/services/PythonVenvService.ts around lines 331 to 334, validate the
incoming pythonVersion before pushing it into args: ensure it's a non-empty
string and matches an allowed whitelist pattern (e.g.
/^python(\d+(\.\d+)?)?$|^\/[A-Za-z0-9_\/\-.]+$/ to permit named interpreters
like "python3.8" or absolute paths), and if it fails validation either throw a
clear Error or skip adding the flag; this prevents illegal inputs from being
passed to the command and improves robustness.

Comment on lines +459 to +468
private async executeCommand(command: string, args: string[], cwd?: string): Promise<string> {
const { spawn } = await import('child_process')

return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd,
stdio: 'pipe',
shell: process.platform === 'win32'
})

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

严重:Windows 下使用 shell 执行带入参命令存在注入风险

executeCommand 在 win32 下启用了 shell:true,且 pythonVersion 源自外部参数。组合后经 shell 传递,存在命令注入风险。建议禁用 shell 并仅以可执行文件+参数方式调用。

应用此修复:

-      const child = spawn(command, args, {
-        cwd,
-        stdio: 'pipe',
-        shell: process.platform === 'win32'
-      })
+      const child = spawn(command, args, {
+        cwd,
+        stdio: 'pipe',
+        shell: false,
+        windowsHide: true
+      })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private async executeCommand(command: string, args: string[], cwd?: string): Promise<string> {
const { spawn } = await import('child_process')
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd,
stdio: 'pipe',
shell: process.platform === 'win32'
})
private async executeCommand(command: string, args: string[], cwd?: string): Promise<string> {
const { spawn } = await import('child_process')
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd,
stdio: 'pipe',
shell: false,
windowsHide: true
})
🤖 Prompt for AI Agents
In src/main/services/PythonVenvService.ts around lines 459 to 468, the current
executeCommand enables shell:true on Windows which allows untrusted
pythonVersion/other inputs to be interpreted by the shell and creates a
command-injection risk; change the spawn call to avoid using the shell (remove
or set shell:false) and invoke the Python executable directly with the args
array (ensure the command is the executable path and all user-controlled data
goes only into the args array), and optionally validate/sanitize pythonVersion
or resolve it to a safe absolute path before calling spawn.

Comment on lines +380 to +415
"download": {
"cancel": "取消下载",
"cancelled": "下载已取消",
"downloading": "下载中",
"failed": "下载失败,请重试",
"success": "下载完成"
},
"path": {
"browse": "浏览",
"browse_title": "选择 FFprobe 可执行文件",
"invalid": "路径无效或文件不存在",
"label": "路径",
"placeholder": "FFprobe 路径将在下载后自动填入,也可手动指定",
"valid": "路径验证成功",
"validation_failed": "路径验证失败"
},
"status": {
"downloading": "下载中...",
"installed": "已安装",
"label": "状态",
"loading": "检测中...",
"not_installed": "未安装",
"system_version": "系统版本",
"unknown": "状态未知"
},
"title": "媒体分析组件 (FFprobe)",
"uninstall": {
"button": "卸载",
"confirm": "确认卸载",
"confirm_description": "此操作将删除已下载的 FFprobe 文件,但不会影响系统安装的版本。",
"confirm_title": "确认卸载 FFprobe?",
"failed": "卸载失败,请重试",
"success": "FFprobe 卸载成功"
},
"version": "版本"
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

补充“安装”文案的翻译键

前端组件会改为读取 settings.plugins.ffprobe.download.install,这里需要同步新增对应键值(例如 "install": "安装 FFprobe")。否则调用会返回空字符串。

🤖 Prompt for AI Agents
In src/renderer/src/i18n/locales/zh-cn.json around lines 380 to 415, the
translations for the FFprobe download object are missing the "install" key
referenced by settings.plugins.ffprobe.download.install; add an "install" entry
(e.g., "install": "安装 FFprobe") under the "download" object so the frontend
returns the correct localized string instead of an empty value.

@mkdir700 mkdir700 force-pushed the feat/media-server-runtime branch from d013f89 to ecd57ee Compare October 12, 2025 02:33
@mkdir700 mkdir700 force-pushed the feat/media-server-runtime branch from ecd57ee to 448a647 Compare October 12, 2025 02:46
mkdir700 and others added 2 commits October 12, 2025 11:54
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 448a647 and 06406ba.

📒 Files selected for processing (2)
  • src/renderer/src/pages/settings/FFmpegSection.tsx (1 hunks)
  • src/renderer/src/pages/settings/FFprobeSection.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/renderer/src/**/*.{ts,tsx,scss,css}

📄 CodeRabbit inference engine (CLAUDE.md)

优先使用 CSS 变量,避免硬编码样式值(颜色等)

Files:

  • src/renderer/src/pages/settings/FFprobeSection.tsx
  • src/renderer/src/pages/settings/FFmpegSection.tsx
src/renderer/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

尺寸与时长等不要硬编码,优先使用 useTheme() 的 token 或集中样式变量(如 motionDurationMid、borderRadiusSM/MD)

Files:

  • src/renderer/src/pages/settings/FFprobeSection.tsx
  • src/renderer/src/pages/settings/FFmpegSection.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: 定制 antd 组件样式优先使用 styled-components 包装 styled(Component),避免全局 classNames
项目的图标统一使用 lucide-react,不使用 emoji 作为图标
组件/Hook 顶层必须通过 useStore(selector) 使用 Zustand,禁止在 useMemo/useEffect 内部调用 store Hook
避免使用返回对象的 Zustand 选择器(如 useStore(s => ({ a: s.a, b: s.b })));应使用单字段选择器或配合 shallow 比较器
遵循 React「副作用与状态更新」规范:渲染纯函数、Effect 三分法、幂等更新、稳定引用、严格清理、禁止写回自身依赖、Provider 值 memo、外部状态 selector 稳定等
统一使用 loggerService 记录日志而不是 console
logger 使用示例中第二个参数必须为对象字面量(如 logger.error('msg', { error }))
任何组件或页面都不要写入 media 元素的 currentTime,播放器控制由编排器统一负责
在 styled-components 中:主题相关属性使用 AntD CSS 变量(如 var(--ant-color-bg-elevated));
在 styled-components 中:设计系统常量(尺寸、动画、层级、字体、毛玻璃等)使用 JS 常量(如 SPACING、BORDER_RADIUS、Z_INDEX、FONT_SIZES 等)

Files:

  • src/renderer/src/pages/settings/FFprobeSection.tsx
  • src/renderer/src/pages/settings/FFmpegSection.tsx
🧬 Code graph analysis (2)
src/renderer/src/pages/settings/FFprobeSection.tsx (4)
src/renderer/src/services/Logger.ts (2)
  • loggerService (817-817)
  • error (422-424)
src/renderer/src/contexts/theme.context.tsx (1)
  • useTheme (124-124)
src/renderer/src/pages/settings/AboutSettings.tsx (1)
  • SettingRowTitle (412-424)
src/renderer/src/infrastructure/styles/theme.ts (6)
  • SPACING (43-58)
  • BORDER_RADIUS (61-72)
  • ANIMATION_DURATION (91-100)
  • EASING (103-114)
  • FONT_WEIGHTS (11-22)
  • FONT_SIZES (25-40)
src/renderer/src/pages/settings/FFmpegSection.tsx (3)
src/renderer/src/services/Logger.ts (2)
  • loggerService (817-817)
  • error (422-424)
src/renderer/src/contexts/theme.context.tsx (1)
  • useTheme (124-124)
src/renderer/src/infrastructure/styles/theme.ts (6)
  • SPACING (43-58)
  • BORDER_RADIUS (61-72)
  • ANIMATION_DURATION (91-100)
  • EASING (103-114)
  • FONT_WEIGHTS (11-22)
  • FONT_SIZES (25-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test (windows-latest, 20)
  • GitHub Check: test (ubuntu-latest, 20)
  • GitHub Check: test (macos-latest, 20)

Comment on lines +152 to +160
setTimeout(() => {
setIsDownloading(false)
setShowSuccessState(false)
setFFmpegStatus(currentStatus)
// 更新 FFmpeg 路径为下载后的路径
setFFmpegPath(currentStatus.path)
// 自动开始预热
handleWarmup()
}, 2000)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

清理成功状态定时器防止卸载后 setState

下载完成后用 setTimeout 延迟复位,但没有保存并清理句柄。若组件在 2 秒内卸载,会对已卸载组件执行 setState。建议与 FFprobeSection 类似,使用 useRef 保存定时器,并在新下载开始或组件卸载时 clearTimeout

-            setTimeout(() => {
+            if (successTimeoutRef.current) {
+              clearTimeout(successTimeoutRef.current)
+            }
+            successTimeoutRef.current = setTimeout(() => {
               setIsDownloading(false)
               setShowSuccessState(false)
               setFFmpegStatus(currentStatus)
               // 更新 FFmpeg 路径为下载后的路径
               setFFmpegPath(currentStatus.path)
               // 自动开始预热
               handleWarmup()
-            }, 2000)
+            }, 2000)

顶部补充:

 const isCancellingRef = useRef(false)
 const isCompletionHandledRef = useRef(false)
+const successTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)

并新增清理逻辑:

 useEffect(() => {
   fetchFFmpegStatus()
 }, [fetchFFmpegStatus, refreshKey])
+
+useEffect(() => {
+  return () => {
+    if (successTimeoutRef.current) {
+      clearTimeout(successTimeoutRef.current)
+    }
+  }
+}, [])
🤖 Prompt for AI Agents
In src/renderer/src/pages/settings/FFmpegSection.tsx around lines 152-160, the
setTimeout used to reset success state isn't stored or cleared, allowing
setState on an unmounted component; store the timeout id in a useRef, assign the
timer to that ref when scheduling the 2s reset, call clearTimeout on that ref
before starting a new download and in a useEffect cleanup (component unmount),
and null out the ref after clearing so the delayed setState won't run after
unmount or when a new download begins.

Comment on lines +237 to +240
title: t('settings.plugins.ffmpeg.path.browse_title'),
properties: ['openFile'],
filters: [{ name: 'FFmpeg 可执行文件', extensions: ['exe', 'app', '*'] }]
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

复用翻译键避免硬编码中文

文件选择对话框名称写死为“FFmpeg 可执行文件”,会导致非中文语言界面出现中文。请改用翻译函数,并在语言包中新增相应键值,例如 settings.plugins.ffmpeg.path.file_filter

-        filters: [{ name: 'FFmpeg 可执行文件', extensions: ['exe', 'app', '*'] }]
+        filters: [
+          {
+            name: t('settings.plugins.ffmpeg.path.file_filter'),
+            extensions: ['exe', 'app', '*']
+          }
+        ]

(As per coding guidelines)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
title: t('settings.plugins.ffmpeg.path.browse_title'),
properties: ['openFile'],
filters: [{ name: 'FFmpeg 可执行文件', extensions: ['exe', 'app', '*'] }]
})
title: t('settings.plugins.ffmpeg.path.browse_title'),
properties: ['openFile'],
filters: [
{
name: t('settings.plugins.ffmpeg.path.file_filter'),
extensions: ['exe', 'app', '*']
}
]
})
🤖 Prompt for AI Agents
In src/renderer/src/pages/settings/FFmpegSection.tsx around lines 237 to 240,
the file dialog filter name is hardcoded as "FFmpeg 可执行文件"; replace that literal
with the translation call (e.g. t('settings.plugins.ffmpeg.path.file_filter'))
so the dialog label is localized, update the language JSON files to add the new
key settings.plugins.ffmpeg.path.file_filter with appropriate translations, and
ensure the filters array uses that translated string instead of the Chinese
literal.

Comment on lines +120 to +126
setTimeout(() => {
setIsDownloading(false)
setShowSuccessState(false)
setFFprobeStatus(currentStatus)
// 更新 FFprobe 路径为下载后的路径
setFFprobePath(currentStatus.path)
}, 2000)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

为成功状态的定时器添加清理逻辑

setTimeout 创建后未保存句柄并在组件卸载时清理,下载流程结束前如果页面跳转,后续的 setState 会对已卸载组件执行。建议用 useRef 记录定时器并在 useEffect 清理,下载流程重新开始前也应 clearTimeout

-            setTimeout(() => {
+            if (successTimeoutRef.current) {
+              clearTimeout(successTimeoutRef.current)
+            }
+            successTimeoutRef.current = setTimeout(() => {
               setIsDownloading(false)
               setShowSuccessState(false)
               setFFprobeStatus(currentStatus)
               // 更新 FFprobe 路径为下载后的路径
               setFFprobePath(currentStatus.path)
-            }, 2000)
+            }, 2000)

并在顶部新增:

 const isCancellingRef = useRef(false)
 const isCompletionHandledRef = useRef(false)
+const successTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)

最后在组件末尾补充:

 useEffect(() => {
   fetchFFprobeStatus()
 }, [fetchFFprobeStatus, refreshKey])
+
+useEffect(() => {
+  return () => {
+    if (successTimeoutRef.current) {
+      clearTimeout(successTimeoutRef.current)
+    }
+  }
+}, [])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setTimeout(() => {
setIsDownloading(false)
setShowSuccessState(false)
setFFprobeStatus(currentStatus)
// 更新 FFprobe 路径为下载后的路径
setFFprobePath(currentStatus.path)
}, 2000)
if (successTimeoutRef.current) {
clearTimeout(successTimeoutRef.current)
}
successTimeoutRef.current = setTimeout(() => {
setIsDownloading(false)
setShowSuccessState(false)
setFFprobeStatus(currentStatus)
// 更新 FFprobe 路径为下载后的路径
setFFprobePath(currentStatus.path)
}, 2000)
🤖 Prompt for AI Agents
In src/renderer/src/pages/settings/FFprobeSection.tsx around lines 120-126, the
setTimeout used to reset download state is not tracked or cleared, which can
call setState after the component unmounts; create a timerRef using
useRef<number | null>(null) at the top of the component, store the timeout id in
timerRef when calling setTimeout, call clearTimeout(timerRef.current) before
starting any new timeout (e.g., when a new download begins or status changes),
and add a useEffect cleanup at the component end that clears the timeout and
sets timerRef.current = null on unmount; ensure types align with
window.setTimeout return type for the project.

Comment on lines +203 to +206
title: t('settings.plugins.ffprobe.path.browse_title'),
properties: ['openFile'],
filters: [{ name: 'FFprobe 可执行文件', extensions: ['exe', 'app', '*'] }]
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

替换文件对话框标题为翻译键

这里直接写死了“FFprobe 可执行文件”,绕过了现有 i18n 体系,非中文环境会看到中文文案。请改为使用 t(...) 并在语言包中补足键值,例如 settings.plugins.ffprobe.path.file_filter

-        filters: [{ name: 'FFprobe 可执行文件', extensions: ['exe', 'app', '*'] }]
+        filters: [
+          {
+            name: t('settings.plugins.ffprobe.path.file_filter'),
+            extensions: ['exe', 'app', '*']
+          }
+        ]

(As per coding guidelines)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
title: t('settings.plugins.ffprobe.path.browse_title'),
properties: ['openFile'],
filters: [{ name: 'FFprobe 可执行文件', extensions: ['exe', 'app', '*'] }]
})
title: t('settings.plugins.ffprobe.path.browse_title'),
properties: ['openFile'],
filters: [
{
name: t('settings.plugins.ffprobe.path.file_filter'),
extensions: ['exe', 'app', '*']
}
]
})
🤖 Prompt for AI Agents
In src/renderer/src/pages/settings/FFprobeSection.tsx around lines 203 to 206,
the file dialog filter label is hard-coded as "FFprobe 可执行文件"; replace this
literal with the i18n call t('settings.plugins.ffprobe.path.file_filter') (or
the agreed key) so the UI uses translations, and then add the corresponding
key/value to all language resource files (e.g. en, zh, etc.) with appropriate
translated strings; keep the rest of the filters array unchanged and ensure the
t(...) import/context is available in this file.

mkdir700 and others added 5 commits October 12, 2025 12:04
…kage

- Create packages/shared/types/system.ts with Platform and Arch type definitions
- Remove duplicate type definitions from FFmpegDownloadService
- Remove duplicate type definitions from UvBootstrapperService
- Import shared types from @shared/types/system in both services

This change improves code maintainability by centralizing common system type definitions.
Ensure media server port is properly cleared and frontend is notified
when the process stops or crashes to prevent stale port usage.

Changes:
- Update notifyPortChanged signature to accept number | null
- Clear this.port to null in both normal stop and crash exit handlers
- Notify frontend with null port when server stops
- Update SessionService listener to handle null port notifications
- Add appropriate logging for port clearing events

This prevents the frontend from continuing to use expired ports after
the media server has stopped, requiring manual app restart.
Fix potential memory leak and unmounted component state update in
MediaServerSection by properly managing setTimeout lifecycle:

- Add successTimeoutRef to store timeout handle
- Clear timeout in effect cleanup on component unmount
- Clear timeout before creating new one in handleInstall
- Clear timeout in error handler to prevent orphaned timers
- Set ref to null after clearing to maintain clean state

This prevents "Can't perform a React state update on an unmounted
component" warnings and ensures proper cleanup of async operations.

Co-Authored-By: Claude <noreply@anthropic.com>
…rtup

- Add 'starting' status immediately after installation completes
- Move fetchServerInfo() to finally block to ensure status refresh
- Prevent confusing 'stopped' status display during startup process
Remove finally block that immediately clears installProgress, preventing callers from reading final "completed" or "error" state. Instead:
- Clear installProgress at start of new installation
- Preserve final state after completion/error for caller consumption
- Remove unnecessary finally block cleanup

This ensures MediaServerSection and other callers can reliably read the final installation state.

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/services/FFmpegDownloadService.ts (1)

49-159: 请固定版本并补齐哈希校验,避免供应链风险

这里依旧使用 latest 与第三方源且未做 SHA256 校验,新加入的 FFprobe 表也沿用了同样模式。任何上游变更都会直接影响用户,甚至可能被投毒。建议:1)改为显式写死版本号与下载地址;2)为每个版本填入官方公布的 SHA256;3)在 performDownload 中下载完成后进入 verifying 阶段对比哈希(crypto.createHash('sha256'))。只有校验通过再继续解压,否则失败并清理临时文件。这两步缺一不可。

Also applies to: 219-275

♻️ Duplicate comments (1)
src/main/services/PythonVenvService.ts (1)

460-468: 仍需移除 Windows shell=true 的命令注入风险

这里依旧在 Windows 下以 shell: true 运行 spawninitializeEnvironment()/createVenv() 会把 UI 传入的 pythonVersion 等参数直接塞进 args,经过 cmd.exe 时可以被构造为恶意命令片段,导致任意代码执行。这一问题之前已提示过,请务必修正。

同样的修复方式:

-      const child = spawn(command, args, {
-        cwd,
-        stdio: 'pipe',
-        shell: process.platform === 'win32'
-      })
+      const child = spawn(command, args, {
+        cwd,
+        stdio: 'pipe',
+        shell: false,
+        windowsHide: true
+      })

结合 uvBootstrapperService 的调整即可统一规避注入风险。

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06406ba and 1f630a0.

📒 Files selected for processing (7)
  • packages/shared/types/system.ts (1 hunks)
  • src/main/services/FFmpegDownloadService.ts (23 hunks)
  • src/main/services/MediaServerService.ts (1 hunks)
  • src/main/services/PythonVenvService.ts (1 hunks)
  • src/main/services/UvBootstrapperService.ts (1 hunks)
  • src/renderer/src/pages/settings/MediaServerSection.tsx (1 hunks)
  • src/renderer/src/services/SessionService.ts (5 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/renderer/src/**/*.{ts,tsx,scss,css}

📄 CodeRabbit inference engine (CLAUDE.md)

优先使用 CSS 变量,避免硬编码样式值(颜色等)

Files:

  • src/renderer/src/pages/settings/MediaServerSection.tsx
  • src/renderer/src/services/SessionService.ts
src/renderer/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

尺寸与时长等不要硬编码,优先使用 useTheme() 的 token 或集中样式变量(如 motionDurationMid、borderRadiusSM/MD)

Files:

  • src/renderer/src/pages/settings/MediaServerSection.tsx
  • src/renderer/src/services/SessionService.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: 定制 antd 组件样式优先使用 styled-components 包装 styled(Component),避免全局 classNames
项目的图标统一使用 lucide-react,不使用 emoji 作为图标
组件/Hook 顶层必须通过 useStore(selector) 使用 Zustand,禁止在 useMemo/useEffect 内部调用 store Hook
避免使用返回对象的 Zustand 选择器(如 useStore(s => ({ a: s.a, b: s.b })));应使用单字段选择器或配合 shallow 比较器
遵循 React「副作用与状态更新」规范:渲染纯函数、Effect 三分法、幂等更新、稳定引用、严格清理、禁止写回自身依赖、Provider 值 memo、外部状态 selector 稳定等
统一使用 loggerService 记录日志而不是 console
logger 使用示例中第二个参数必须为对象字面量(如 logger.error('msg', { error }))
任何组件或页面都不要写入 media 元素的 currentTime,播放器控制由编排器统一负责
在 styled-components 中:主题相关属性使用 AntD CSS 变量(如 var(--ant-color-bg-elevated));
在 styled-components 中:设计系统常量(尺寸、动画、层级、字体、毛玻璃等)使用 JS 常量(如 SPACING、BORDER_RADIUS、Z_INDEX、FONT_SIZES 等)

Files:

  • src/renderer/src/pages/settings/MediaServerSection.tsx
  • src/main/services/UvBootstrapperService.ts
  • src/main/services/MediaServerService.ts
  • src/main/services/PythonVenvService.ts
  • src/main/services/FFmpegDownloadService.ts
  • packages/shared/types/system.ts
  • src/renderer/src/services/SessionService.ts
🧬 Code graph analysis (6)
src/renderer/src/pages/settings/MediaServerSection.tsx (6)
src/renderer/src/services/Logger.ts (3)
  • loggerService (817-817)
  • info (436-438)
  • error (422-424)
src/main/services/MediaServerService.ts (1)
  • MediaServerInfo (29-36)
src/main/services/PythonVenvService.ts (1)
  • InstallProgress (25-29)
src/renderer/src/contexts/theme.context.tsx (1)
  • useTheme (124-124)
src/renderer/src/pages/settings/AboutSettings.tsx (1)
  • SettingRowTitle (412-424)
src/renderer/src/infrastructure/styles/theme.ts (6)
  • SPACING (43-58)
  • BORDER_RADIUS (61-72)
  • ANIMATION_DURATION (91-100)
  • EASING (103-114)
  • FONT_WEIGHTS (11-22)
  • FONT_SIZES (25-40)
src/main/services/UvBootstrapperService.ts (3)
src/renderer/src/services/Logger.ts (2)
  • loggerService (817-817)
  • error (422-424)
packages/shared/types/system.ts (2)
  • Platform (2-2)
  • Arch (3-3)
src/main/services/FFmpegDownloadService.ts (1)
  • DownloadProgress (25-32)
src/main/services/MediaServerService.ts (5)
src/renderer/src/services/Logger.ts (2)
  • loggerService (817-817)
  • info (436-438)
src/main/services/FFmpegDownloadService.ts (1)
  • FFmpegDownloadService (277-1047)
src/main/utils/index.ts (1)
  • getDataPath (11-17)
src/main/services/UvBootstrapperService.ts (1)
  • uvBootstrapperService (1185-1185)
src/main/services/PythonVenvService.ts (1)
  • pythonVenvService (519-519)
src/main/services/PythonVenvService.ts (2)
src/renderer/src/services/Logger.ts (1)
  • loggerService (817-817)
src/main/services/UvBootstrapperService.ts (1)
  • uvBootstrapperService (1185-1185)
src/main/services/FFmpegDownloadService.ts (2)
packages/shared/types/system.ts (2)
  • Platform (2-2)
  • Arch (3-3)
src/main/services/UvBootstrapperService.ts (1)
  • DownloadProgress (23-30)
src/renderer/src/services/SessionService.ts (1)
src/renderer/src/services/Logger.ts (2)
  • loggerService (817-817)
  • error (422-424)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test (macos-latest, 20)
  • GitHub Check: test (windows-latest, 20)
  • GitHub Check: test (ubuntu-latest, 20)
🔇 Additional comments (2)
src/renderer/src/pages/settings/MediaServerSection.tsx (2)

127-183: 定时器清理已正确实现

轮询安装进度的 Effect 现已正确清理所有定时器:

  • 旧的 setTimeout 在创建新定时器前被清除(153-157 行)
  • cleanup 函数清理 interval 和 timeout(174-182 行)
  • 所有相关清理点(handleInstall、错误处理)都正确处理了 timeout

这解决了之前审查中提到的"组件卸载后状态更新"问题。


505-607: 样式组件遵循设计规范

样式组件正确使用了设计系统:

  • 使用 CSS 变量处理主题相关属性(如 var(--ant-color-success)var(--ant-color-primary)
  • 使用 JS 常量处理设计系统常量(SPACINGBORDER_RADIUSFONT_SIZESANIMATION_DURATIONEASING
  • 通过 styled(Button) 包装 antd 组件以实现定制

完全符合编码规范。

基于编码规范

Comment on lines +339 to +372
this.process.on('exit', (code, signal) => {
logger.warn('Media Server 进程退出', { code, signal })

const previousPort = this.port

this.status = 'stopped'
this.process = null
this.startTime = null
this.port = null

// 通知前端端口已失效
if (previousPort !== null) {
this.notifyPortChanged(null)
}

// 如果是异常退出,尝试重启
if (code !== 0 && this.restartAttempts < this.MAX_RESTART_ATTEMPTS) {
this.restartAttempts++
logger.info('尝试重启 Media Server', {
attempt: this.restartAttempts,
maxAttempts: this.MAX_RESTART_ATTEMPTS
})

setTimeout(() => {
this.start(finalConfig).catch((error) => {
logger.error('重启 Media Server 失败', { error })
})
}, 2000 * this.restartAttempts) // 递增延迟
} else if (this.restartAttempts >= this.MAX_RESTART_ATTEMPTS) {
logger.error('Media Server 重启次数过多,停止重启')
this.lastError = '服务异常退出次数过多'
this.status = 'error'
}
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

阻止 stop() 误触发自重启

调用 stop() 时子进程通常以 code = nullsignal = 'SIGTERM' 退出,但这里直接判断 code !== 0 会为 true,从而排队 this.start(finalConfig)。结果是点击“停止”后服务立刻又被拉起,主进程永远停不下来。这是严重的行为缺陷。

建议在 exit 回调一开始记录 const wasStopping = this.status === 'stopping',并改用 const exitCode = typeof code === 'number' ? code : 0const shouldRestart = !wasStopping && signal === null && exitCode !== 0。只有在 shouldRestart 为真时才递增 restartAttempts 并排队自重启,其他分支(包含人工 stop、SIGTERM、SIGKILL 强杀等)需立刻清零 restartAttempts。示例修复:

-      this.process.on('exit', (code, signal) => {
-        logger.warn('Media Server 进程退出', { code, signal })
+      this.process.on('exit', (code, signal) => {
+        const wasStopping = this.status === 'stopping'
+        logger.warn('Media Server 进程退出', { code, signal })
...
-        if (code !== 0 && this.restartAttempts < this.MAX_RESTART_ATTEMPTS) {
+        const exitCode = typeof code === 'number' ? code : 0
+        const shouldRestart = !wasStopping && signal === null && exitCode !== 0
+
+        if (shouldRestart && this.restartAttempts < this.MAX_RESTART_ATTEMPTS) {
...
-        } else if (this.restartAttempts >= this.MAX_RESTART_ATTEMPTS) {
+        } else if (shouldRestart && this.restartAttempts >= this.MAX_RESTART_ATTEMPTS) {
           ...
-        }
+        } else {
+          this.restartAttempts = 0
+        }

这样手动 stop、不正常崩溃与强制 kill 才会被正确区分。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.process.on('exit', (code, signal) => {
logger.warn('Media Server 进程退出', { code, signal })
const previousPort = this.port
this.status = 'stopped'
this.process = null
this.startTime = null
this.port = null
// 通知前端端口已失效
if (previousPort !== null) {
this.notifyPortChanged(null)
}
// 如果是异常退出,尝试重启
if (code !== 0 && this.restartAttempts < this.MAX_RESTART_ATTEMPTS) {
this.restartAttempts++
logger.info('尝试重启 Media Server', {
attempt: this.restartAttempts,
maxAttempts: this.MAX_RESTART_ATTEMPTS
})
setTimeout(() => {
this.start(finalConfig).catch((error) => {
logger.error('重启 Media Server 失败', { error })
})
}, 2000 * this.restartAttempts) // 递增延迟
} else if (this.restartAttempts >= this.MAX_RESTART_ATTEMPTS) {
logger.error('Media Server 重启次数过多,停止重启')
this.lastError = '服务异常退出次数过多'
this.status = 'error'
}
})
this.process.on('exit', (code, signal) => {
const wasStopping = this.status === 'stopping'
logger.warn('Media Server 进程退出', { code, signal })
const previousPort = this.port
this.status = 'stopped'
this.process = null
this.startTime = null
this.port = null
// 通知前端端口已失效
if (previousPort !== null) {
this.notifyPortChanged(null)
}
// 如果是异常退出,尝试重启
const exitCode = typeof code === 'number' ? code : 0
const shouldRestart = !wasStopping && signal === null && exitCode !== 0
if (shouldRestart && this.restartAttempts < this.MAX_RESTART_ATTEMPTS) {
this.restartAttempts++
logger.info('尝试重启 Media Server', {
attempt: this.restartAttempts,
maxAttempts: this.MAX_RESTART_ATTEMPTS
})
setTimeout(() => {
this.start(finalConfig).catch((error) => {
logger.error('重启 Media Server 失败', { error })
})
}, 2000 * this.restartAttempts) // 递增延迟
} else if (shouldRestart && this.restartAttempts >= this.MAX_RESTART_ATTEMPTS) {
logger.error('Media Server 重启次数过多,停止重启')
this.lastError = '服务异常退出次数过多'
this.status = 'error'
} else {
// 手动 stop 或强杀等场景,不累计重启次数
this.restartAttempts = 0
}
})
🤖 Prompt for AI Agents
In src/main/services/MediaServerService.ts around lines 339-372, the exit
handler treats any non-zero code as a crash which causes stop() (which yields
code=null and signal='SIGTERM') to trigger an unwanted restart; change the logic
to first capture const wasStopping = this.status === 'stopping', then normalize
the exit code with const exitCode = typeof code === 'number' ? code : 0, compute
const shouldRestart = !wasStopping && signal === null && exitCode !== 0, and
only when shouldRestart is true increment restartAttempts and schedule
this.start(finalConfig); in all other cases (including manual stop,
SIGTERM/SIGKILL) reset this.restartAttempts = 0 and avoid scheduling a restart;
keep the existing status/port clearing and notifyPortChanged behavior untouched
and set status='error' only when restartAttempts exceeds MAX_RESTART_ATTEMPTS.

Comment on lines +343 to +378
private async executeCommand(command: string, args: string[], cwd?: string): Promise<string> {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd,
stdio: 'pipe',
shell: process.platform === 'win32'
})

let stdout = ''
let stderr = ''

child.stdout?.on('data', (data) => {
stdout += data.toString()
})

child.stderr?.on('data', (data) => {
stderr += data.toString()
})

child.on('close', (code) => {
if (code === 0) {
resolve(stdout.trim())
} else {
reject(
new Error(
`命令执行失败: ${command} ${args.join(' ')}\n退出代码: ${code}\n错误输出: ${stderr}`
)
)
}
})

child.on('error', (error) => {
reject(error)
})
})
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

关闭 Windows shell=true 的注入面

executeCommand 在 Windows 下仍使用 shell: true。当后续调用诸如 installPackage() 时,packageNameversion 等来自用户/配置的字符串会直接经 cmd.exe 解析,一旦字符串里包含诸如 "; calc.exe; " 等片段,就能突破引号约束把兼容命令注入进来,导致任意命令执行。这是严重安全漏洞。

请改为直接调用二进制,禁用 shell,并顺带隐藏窗口。例如:

-      const child = spawn(command, args, {
-        cwd,
-        stdio: 'pipe',
-        shell: process.platform === 'win32'
-      })
+      const child = spawn(command, args, {
+        cwd,
+        stdio: 'pipe',
+        shell: false,
+        windowsHide: true
+      })

如需 PATH 解析,可保持 command 为 'uv'/'where' 等,Node 会处理 PATHEXT。这样可彻底消除此注入面。

🤖 Prompt for AI Agents
In src/main/services/UvBootstrapperService.ts around lines 343-378, the current
executeCommand enables shell on Windows which permits command injection via
untrusted args; change the spawn call to invoke the binary directly (no shell)
and add windowsHide: true to avoid showing a window on Windows: remove or set
shell: false, ensure args are passed only via the args array (do not interpolate
user/config strings into a single command string), keep stdio: 'pipe', and
preserve the same stdout/stderr handling and exit-code rejection so callers
receive the same error details.

Comment on lines +186 to +329
const handleInstall = useCallback(async () => {
try {
// 清理之前的 timeout(如果存在)
if (successTimeoutRef.current) {
clearTimeout(successTimeoutRef.current)
successTimeoutRef.current = null
}

isCompletionHandledRef.current = false
setIsInstalling(true)
updateInstallProgress({ stage: 'init', message: '正在检查依赖...', percent: 0 })

const ensureDependency = async (
type: DependencyType,
label: 'FFprobe' | 'FFmpeg',
startPercent: number,
completionPercent: number
) => {
const checkExists =
type === 'ffprobe'
? () => window.api.ffprobe.checkExists()
: () => window.api.ffmpeg.checkExists()
const download =
type === 'ffprobe'
? () => window.api.ffprobe.download.download()
: () => window.api.ffmpeg.download.download()

logger.info(`检查 ${label} 安装状态`)
const exists = await checkExists()

if (exists) {
logger.info(`${label} 已安装,跳过下载`)
updateInstallProgress({
stage: 'deps',
message: `${label} 已就绪`,
percent: completionPercent
})
onDependencyReady?.(type)
return
}

logger.info(`${label} 未安装,开始下载`)
updateInstallProgress({
stage: 'deps',
message: `正在安装 ${label}...`,
percent: startPercent
})
message.info(`正在安装 ${label},请稍候...`)

const result = await download()
if (!result) {
const installedAfterAttempt = await checkExists()
if (!installedAfterAttempt) {
throw new Error(`${label} 安装失败,请先完成 ${label} 安装后重试`)
}
}

updateInstallProgress({
stage: 'deps',
message: `${label} 安装完成`,
percent: completionPercent
})
message.success(`${label} 安装完成`)
onDependencyReady?.(type)
}

// 依赖安装:先安装 FFprobe,再安装 FFmpeg
await ensureDependency('ffprobe', 'FFprobe', 5, 10)
await ensureDependency('ffmpeg', 'FFmpeg', 15, 20)

updateInstallProgress({ stage: 'init', message: '正在检查环境...', percent: 25 })

// 步骤 1: 检查并安装 UV
logger.info('检查 UV 安装状态')
const uvInfo = await window.api.uv.checkInstallation()

if (!uvInfo.exists) {
logger.info('UV 未安装,开始下载...')
updateInstallProgress({ stage: 'init', message: '正在下载 UV 包管理器...', percent: 35 })

// 下载 UV
const uvDownloaded = await window.api.uv.download()
if (!uvDownloaded) {
throw new Error('UV 下载失败')
}

logger.info('UV 下载完成')
updateInstallProgress({ stage: 'init', message: 'UV 安装完成', percent: 45 })
} else {
logger.info('UV 已存在,跳过下载', { uvInfo })
updateInstallProgress({ stage: 'init', message: 'UV 已就绪', percent: 45 })
}

// 步骤 2: 初始化 Python 环境(uv 会自动下载匹配的 Python 解释器)
logger.info('开始初始化 Python 环境')
updateInstallProgress({ stage: 'init', message: '正在初始化 Python 环境...', percent: 55 })

const result = await window.api.pythonVenv.initialize()
if (!result) {
throw new Error('Python 环境初始化失败')
}

logger.info('Media Server 环境安装完成')

// 步骤 3: 安装完成后自动启动 Media Server
logger.info('准备启动 Media Server')
updateInstallProgress({
stage: 'completed',
message: '正在启动 Media Server...',
percent: 95
})

// 先设置状态为"启动中",避免用户看到"已停止"状态
setServerInfo({ status: 'starting' })

try {
const startResult = await window.api.mediaServer.start()
if (startResult) {
logger.info('Media Server 启动成功')
} else {
logger.warn('Media Server 启动失败,但环境安装成功')
}
} catch (startError) {
logger.error('启动 Media Server 失败,但环境安装成功:', { error: startError })
// 不抛出错误,因为安装已成功
} finally {
// 无论启动成功与否,都刷新服务器状态以显示最新状态
await fetchServerInfo()
}
} catch (error) {
// 清理 timeout
if (successTimeoutRef.current) {
clearTimeout(successTimeoutRef.current)
successTimeoutRef.current = null
}

setIsInstalling(false)
updateInstallProgress(null)
message.error(
error instanceof Error ? error.message : 'Media Server 环境安装失败,请稍后重试'
)
logger.error('安装 Media Server 环境失败:', { error })
}
}, [fetchServerInfo, onDependencyReady, updateInstallProgress])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

考虑提取辅助函数以降低复杂度

handleInstall 函数较长(144 行),编排了多个安装步骤(依赖检查、下载、UV 安装、虚拟环境初始化、服务器启动)。虽然当前实现逻辑清晰且正确,但可以考虑将 ensureDependency 等辅助逻辑提取为组件外部或自定义 Hook 中的独立函数,以提升可测试性和可维护性。

这是一个可选的代码组织改进,不影响当前功能的正确性。

🤖 Prompt for AI Agents
In src/renderer/src/pages/settings/MediaServerSection.tsx around lines 186 to
329, the handleInstall function is large and should be refactored: extract the
ensureDependency logic into a standalone helper (or hook) that accepts
parameters (type, label, startPercent, completionPercent, onDependencyReady,
updateInstallProgress, message, logger, window.api) and returns a promise;
extract the UV install/check block, the Python venv initialization, and the
media server start/refresh logic into separate functions (or methods in the
hook) that accept the minimal state setters and utilities they need
(setIsInstalling, setServerInfo, fetchServerInfo, updateInstallProgress,
message, logger, window.api); update handleInstall to call these helpers in
sequence and handle the try/catch and timeout cleanup as before; ensure refs
(successTimeoutRef, isCompletionHandledRef) and callbacks (onDependencyReady,
fetchServerInfo) are passed in or referenced from the hook, and add small unit
tests for each extracted function to validate success/failure flows.

- Create shared types module for Media Server and Python Venv
- Add media-server.ts with MediaServerInfo, MediaServerStatus, PythonVenvInfo, and InstallProgress types
- Remove duplicate type definitions from MediaServerService, PythonVenvService, and MediaServerSection
- Update imports to use @shared/types across main and renderer processes
- Maintain backward compatibility by re-exporting types from service files
- All type checks passing
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/main/services/MediaServerService.ts (1)

318-350: 修复 stop() 后被误判为崩溃重启的问题

stop() 会让子进程以 code = nullsignal = 'SIGTERM' 退出,当前逻辑把它当成 code !== 0 的异常重启,导致手动停止后服务立刻又被拉起。请在退出回调中识别 this.status === 'stopping' 并只在真正崩溃时重试。

-      this.process.on('exit', (code, signal) => {
-        logger.warn('Media Server 进程退出', { code, signal })
+      this.process.on('exit', (code, signal) => {
+        const wasStopping = this.status === 'stopping'
+        logger.warn('Media Server 进程退出', { code, signal, wasStopping })
...
-        if (code !== 0 && this.restartAttempts < this.MAX_RESTART_ATTEMPTS) {
+        const exitCode = typeof code === 'number' ? code : 0
+        const shouldRestart = !wasStopping && signal === null && exitCode !== 0
+
+        if (shouldRestart && this.restartAttempts < this.MAX_RESTART_ATTEMPTS) {
...
-        } else if (this.restartAttempts >= this.MAX_RESTART_ATTEMPTS) {
+        } else if (shouldRestart && this.restartAttempts >= this.MAX_RESTART_ATTEMPTS) {
           logger.error('Media Server 重启次数过多,停止重启')
           this.lastError = '服务异常退出次数过多'
           this.status = 'error'
+        } else {
+          this.restartAttempts = 0
         }
src/main/services/PythonVenvService.ts (2)

413-429: 避免在主进程中同步删除大型目录

fs.rmSync 会阻塞事件循环,删除几十/上百 MB 的 venv 时主进程会卡顿。改用异步 Promise 版本并 await,保持主线程响应。

-      // 递归删除目录
-      fs.rmSync(venvPath, { recursive: true, force: true })
+      // 递归删除目录(异步,避免阻塞主进程)
+      await fs.promises.rm(venvPath, { recursive: true, force: true })

440-448: 关闭 Windows shell 执行以消除命令注入风险

在 Windows 下启用 shell: true 会让用户输入(如 pythonVersion)被当作命令拼接,存在注入隐患。请强制 shell: false 并隐藏窗口。

-      const child = spawn(command, args, {
-        cwd,
-        stdio: 'pipe',
-        shell: process.platform === 'win32'
-      })
+      const child = spawn(command, args, {
+        cwd,
+        stdio: 'pipe',
+        shell: false,
+        windowsHide: true
+      })
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f630a0 and 2b254e9.

📒 Files selected for processing (5)
  • packages/shared/types/index.ts (1 hunks)
  • packages/shared/types/media-server.ts (1 hunks)
  • src/main/services/MediaServerService.ts (1 hunks)
  • src/main/services/PythonVenvService.ts (1 hunks)
  • src/renderer/src/pages/settings/MediaServerSection.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: 定制 antd 组件样式优先使用 styled-components 包装 styled(Component),避免全局 classNames
项目的图标统一使用 lucide-react,不使用 emoji 作为图标
组件/Hook 顶层必须通过 useStore(selector) 使用 Zustand,禁止在 useMemo/useEffect 内部调用 store Hook
避免使用返回对象的 Zustand 选择器(如 useStore(s => ({ a: s.a, b: s.b })));应使用单字段选择器或配合 shallow 比较器
遵循 React「副作用与状态更新」规范:渲染纯函数、Effect 三分法、幂等更新、稳定引用、严格清理、禁止写回自身依赖、Provider 值 memo、外部状态 selector 稳定等
统一使用 loggerService 记录日志而不是 console
logger 使用示例中第二个参数必须为对象字面量(如 logger.error('msg', { error }))
任何组件或页面都不要写入 media 元素的 currentTime,播放器控制由编排器统一负责
在 styled-components 中:主题相关属性使用 AntD CSS 变量(如 var(--ant-color-bg-elevated));
在 styled-components 中:设计系统常量(尺寸、动画、层级、字体、毛玻璃等)使用 JS 常量(如 SPACING、BORDER_RADIUS、Z_INDEX、FONT_SIZES 等)

Files:

  • packages/shared/types/media-server.ts
  • packages/shared/types/index.ts
  • src/main/services/MediaServerService.ts
  • src/main/services/PythonVenvService.ts
  • src/renderer/src/pages/settings/MediaServerSection.tsx
src/renderer/src/**/*.{ts,tsx,scss,css}

📄 CodeRabbit inference engine (CLAUDE.md)

优先使用 CSS 变量,避免硬编码样式值(颜色等)

Files:

  • src/renderer/src/pages/settings/MediaServerSection.tsx
src/renderer/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

尺寸与时长等不要硬编码,优先使用 useTheme() 的 token 或集中样式变量(如 motionDurationMid、borderRadiusSM/MD)

Files:

  • src/renderer/src/pages/settings/MediaServerSection.tsx
🧬 Code graph analysis (3)
src/main/services/MediaServerService.ts (6)
src/renderer/src/services/Logger.ts (3)
  • loggerService (817-817)
  • error (422-424)
  • info (436-438)
packages/shared/types/media-server.ts (2)
  • MediaServerStatus (8-13)
  • MediaServerInfo (18-25)
src/main/services/FFmpegDownloadService.ts (1)
  • FFmpegDownloadService (277-1047)
src/main/utils/index.ts (1)
  • getDataPath (11-17)
src/main/services/UvBootstrapperService.ts (1)
  • uvBootstrapperService (1185-1185)
src/main/services/PythonVenvService.ts (1)
  • pythonVenvService (499-499)
src/main/services/PythonVenvService.ts (3)
src/renderer/src/services/Logger.ts (2)
  • loggerService (817-817)
  • error (422-424)
packages/shared/types/media-server.ts (2)
  • InstallProgress (42-46)
  • PythonVenvInfo (30-37)
src/main/services/UvBootstrapperService.ts (1)
  • uvBootstrapperService (1185-1185)
src/renderer/src/pages/settings/MediaServerSection.tsx (5)
src/renderer/src/services/Logger.ts (2)
  • loggerService (817-817)
  • error (422-424)
src/renderer/src/contexts/theme.context.tsx (1)
  • useTheme (124-124)
packages/shared/types/media-server.ts (3)
  • MediaServerInfo (18-25)
  • PythonVenvInfo (30-37)
  • InstallProgress (42-46)
src/renderer/src/pages/settings/AboutSettings.tsx (1)
  • SettingRowTitle (412-424)
src/renderer/src/infrastructure/styles/theme.ts (6)
  • SPACING (43-58)
  • BORDER_RADIUS (61-72)
  • ANIMATION_DURATION (91-100)
  • EASING (103-114)
  • FONT_WEIGHTS (11-22)
  • FONT_SIZES (25-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test (ubuntu-latest, 20)
  • GitHub Check: test (macos-latest, 20)
  • GitHub Check: test (windows-latest, 20)

Comment on lines +148 to +166
private async findAvailablePort(preferredPort?: number): Promise<number> {
// 如果指定了优先端口,先尝试该端口
if (preferredPort && (await this.isPortAvailable(preferredPort))) {
return preferredPort
}

// 随机选择起始端口,避免总是从同一个端口开始
const startPort = Math.floor(Math.random() * (this.MAX_PORT - this.MIN_PORT)) + this.MIN_PORT

// 从随机起始位置开始查找
for (let i = 0; i < this.MAX_PORT - this.MIN_PORT; i++) {
const port =
this.MIN_PORT + ((startPort - this.MIN_PORT + i) % (this.MAX_PORT - this.MIN_PORT))
if (await this.isPortAvailable(port)) {
return port
}
}

throw new Error(`无法在端口范围 ${this.MIN_PORT}-${this.MAX_PORT} 内找到可用端口`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

修正端口遍历范围的 off-by-one 错误

当前循环只覆盖到 8864,最大值 8865 永远不会尝试。请把区间长度加一,并在随机起点和取模时使用同一个 rangeSize

-    const startPort = Math.floor(Math.random() * (this.MAX_PORT - this.MIN_PORT)) + this.MIN_PORT
+    const rangeSize = this.MAX_PORT - this.MIN_PORT + 1
+    const startPort = Math.floor(Math.random() * rangeSize) + this.MIN_PORT
...
-    for (let i = 0; i < this.MAX_PORT - this.MIN_PORT; i++) {
-      const port =
-        this.MIN_PORT + ((startPort - this.MIN_PORT + i) % (this.MAX_PORT - this.MIN_PORT))
+    for (let i = 0; i < rangeSize; i++) {
+      const port = this.MIN_PORT + ((startPort - this.MIN_PORT + i) % rangeSize)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private async findAvailablePort(preferredPort?: number): Promise<number> {
// 如果指定了优先端口,先尝试该端口
if (preferredPort && (await this.isPortAvailable(preferredPort))) {
return preferredPort
}
// 随机选择起始端口,避免总是从同一个端口开始
const startPort = Math.floor(Math.random() * (this.MAX_PORT - this.MIN_PORT)) + this.MIN_PORT
// 从随机起始位置开始查找
for (let i = 0; i < this.MAX_PORT - this.MIN_PORT; i++) {
const port =
this.MIN_PORT + ((startPort - this.MIN_PORT + i) % (this.MAX_PORT - this.MIN_PORT))
if (await this.isPortAvailable(port)) {
return port
}
}
throw new Error(`无法在端口范围 ${this.MIN_PORT}-${this.MAX_PORT} 内找到可用端口`)
private async findAvailablePort(preferredPort?: number): Promise<number> {
// 如果指定了优先端口,先尝试该端口
if (preferredPort && (await this.isPortAvailable(preferredPort))) {
return preferredPort
}
// 随机选择起始端口,避免总是从同一个端口开始
const rangeSize = this.MAX_PORT - this.MIN_PORT + 1
const startPort = Math.floor(Math.random() * rangeSize) + this.MIN_PORT
// 从随机起始位置开始查找
for (let i = 0; i < rangeSize; i++) {
const port = this.MIN_PORT + ((startPort - this.MIN_PORT + i) % rangeSize)
if (await this.isPortAvailable(port)) {
return port
}
}
throw new Error(`无法在端口范围 ${this.MIN_PORT}-${this.MAX_PORT} 内找到可用端口`)
}
🤖 Prompt for AI Agents
In src/main/services/MediaServerService.ts around lines 148 to 166, the
port-search loop has an off-by-one bug that never tests the upper bound
(MAX_PORT); define a rangeSize = this.MAX_PORT - this.MIN_PORT + 1, use it when
computing the random startPort (Math.floor(Math.random() * rangeSize) +
this.MIN_PORT), use the same rangeSize in the modulo and in the loop condition
(for (let i = 0; i < rangeSize; i++)), and keep using isPortAvailable to return
the found port or throw if none found.

@mkdir700 mkdir700 merged commit 8b0ec03 into dev Oct 12, 2025
5 checks passed
@mkdir700 mkdir700 deleted the feat/media-server-runtime branch October 12, 2025 06:31
mkdir700 added a commit that referenced this pull request Oct 12, 2025
* feat(media-server): implement runtime runtime management system

- add UV bootstrapper with cross-platform install, caching, progress, validation

- manage Python venv lifecycle, dependency installs, cleanup, and phased progress

- introduce MediaServerService with lifecycle control, health checks, and port management

- extend FFmpeg service to handle FFprobe with unified downloads and version updates

- build settings UI sections for media server and plugins with real-time status

- wire up 38 IPC channels covering UV, venv, FFmpeg/FFprobe, and server control

- update build configs, add ffprobe download script, auto-start server, and bump backend ref

* test: Update tests

* test(main): mock electron app for ipc handlers

* Update src/renderer/src/pages/settings/FFprobeSection.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Apply suggestion from @coderabbitai[bot]

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* refactor(types): extract common Platform and Arch types to shared package

- Create packages/shared/types/system.ts with Platform and Arch type definitions
- Remove duplicate type definitions from FFmpegDownloadService
- Remove duplicate type definitions from UvBootstrapperService
- Import shared types from @shared/types/system in both services

This change improves code maintainability by centralizing common system type definitions.

* fix(media-server): clear port and notify frontend on process exit

Ensure media server port is properly cleared and frontend is notified
when the process stops or crashes to prevent stale port usage.

Changes:
- Update notifyPortChanged signature to accept number | null
- Clear this.port to null in both normal stop and crash exit handlers
- Notify frontend with null port when server stops
- Update SessionService listener to handle null port notifications
- Add appropriate logging for port clearing events

This prevents the frontend from continuing to use expired ports after
the media server has stopped, requiring manual app restart.

* fix(settings): prevent setTimeout memory leak in MediaServerSection

Fix potential memory leak and unmounted component state update in
MediaServerSection by properly managing setTimeout lifecycle:

- Add successTimeoutRef to store timeout handle
- Clear timeout in effect cleanup on component unmount
- Clear timeout before creating new one in handleInstall
- Clear timeout in error handler to prevent orphaned timers
- Set ref to null after clearing to maintain clean state

This prevents "Can't perform a React state update on an unmounted
component" warnings and ensures proper cleanup of async operations.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(media-server): improve status display during installation and startup

- Add 'starting' status immediately after installation completes
- Move fetchServerInfo() to finally block to ensure status refresh
- Prevent confusing 'stopped' status display during startup process

* fix(python-venv): preserve final install progress state for callers

Remove finally block that immediately clears installProgress, preventing callers from reading final "completed" or "error" state. Instead:
- Clear installProgress at start of new installation
- Preserve final state after completion/error for caller consumption
- Remove unnecessary finally block cleanup

This ensures MediaServerSection and other callers can reliably read the final installation state.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(types): centralize media server types to shared package

- Create shared types module for Media Server and Python Venv
- Add media-server.ts with MediaServerInfo, MediaServerStatus, PythonVenvInfo, and InstallProgress types
- Remove duplicate type definitions from MediaServerService, PythonVenvService, and MediaServerSection
- Update imports to use @shared/types across main and renderer processes
- Maintain backward compatibility by re-exporting types from service files
- All type checks passing
@coderabbitai coderabbitai bot mentioned this pull request Oct 12, 2025
mkdir700 added a commit that referenced this pull request Oct 12, 2025
* feat(media-server): implement runtime runtime management system

- add UV bootstrapper with cross-platform install, caching, progress, validation

- manage Python venv lifecycle, dependency installs, cleanup, and phased progress

- introduce MediaServerService with lifecycle control, health checks, and port management

- extend FFmpeg service to handle FFprobe with unified downloads and version updates

- build settings UI sections for media server and plugins with real-time status

- wire up 38 IPC channels covering UV, venv, FFmpeg/FFprobe, and server control

- update build configs, add ffprobe download script, auto-start server, and bump backend ref

* test: Update tests

* test(main): mock electron app for ipc handlers

* Update src/renderer/src/pages/settings/FFprobeSection.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Apply suggestion from @coderabbitai[bot]

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* refactor(types): extract common Platform and Arch types to shared package

- Create packages/shared/types/system.ts with Platform and Arch type definitions
- Remove duplicate type definitions from FFmpegDownloadService
- Remove duplicate type definitions from UvBootstrapperService
- Import shared types from @shared/types/system in both services

This change improves code maintainability by centralizing common system type definitions.

* fix(media-server): clear port and notify frontend on process exit

Ensure media server port is properly cleared and frontend is notified
when the process stops or crashes to prevent stale port usage.

Changes:
- Update notifyPortChanged signature to accept number | null
- Clear this.port to null in both normal stop and crash exit handlers
- Notify frontend with null port when server stops
- Update SessionService listener to handle null port notifications
- Add appropriate logging for port clearing events

This prevents the frontend from continuing to use expired ports after
the media server has stopped, requiring manual app restart.

* fix(settings): prevent setTimeout memory leak in MediaServerSection

Fix potential memory leak and unmounted component state update in
MediaServerSection by properly managing setTimeout lifecycle:

- Add successTimeoutRef to store timeout handle
- Clear timeout in effect cleanup on component unmount
- Clear timeout before creating new one in handleInstall
- Clear timeout in error handler to prevent orphaned timers
- Set ref to null after clearing to maintain clean state

This prevents "Can't perform a React state update on an unmounted
component" warnings and ensures proper cleanup of async operations.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(media-server): improve status display during installation and startup

- Add 'starting' status immediately after installation completes
- Move fetchServerInfo() to finally block to ensure status refresh
- Prevent confusing 'stopped' status display during startup process

* fix(python-venv): preserve final install progress state for callers

Remove finally block that immediately clears installProgress, preventing callers from reading final "completed" or "error" state. Instead:
- Clear installProgress at start of new installation
- Preserve final state after completion/error for caller consumption
- Remove unnecessary finally block cleanup

This ensures MediaServerSection and other callers can reliably read the final installation state.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(types): centralize media server types to shared package

- Create shared types module for Media Server and Python Venv
- Add media-server.ts with MediaServerInfo, MediaServerStatus, PythonVenvInfo, and InstallProgress types
- Remove duplicate type definitions from MediaServerService, PythonVenvService, and MediaServerSection
- Update imports to use @shared/types across main and renderer processes
- Maintain backward compatibility by re-exporting types from service files
- All type checks passing
github-actions bot pushed a commit that referenced this pull request Oct 12, 2025
# [1.1.0-alpha.2](v1.1.0-alpha.1...v1.1.0-alpha.2) (2025-10-12)

### Features

* **media-server:** implement runtime runtime management system ([#204](#204)) ([2d179f5](2d179f5))
* **player:** add animated loading progress bar to PlayerPage ([#206](#206)) ([53f7393](53f7393))
* **player:** add media server recommendation prompt for incompatible videos ([#205](#205)) ([12b4434](12b4434))
mkdir700 added a commit that referenced this pull request Oct 15, 2025
* feat(media-server): implement runtime runtime management system

- add UV bootstrapper with cross-platform install, caching, progress, validation

- manage Python venv lifecycle, dependency installs, cleanup, and phased progress

- introduce MediaServerService with lifecycle control, health checks, and port management

- extend FFmpeg service to handle FFprobe with unified downloads and version updates

- build settings UI sections for media server and plugins with real-time status

- wire up 38 IPC channels covering UV, venv, FFmpeg/FFprobe, and server control

- update build configs, add ffprobe download script, auto-start server, and bump backend ref

* test: Update tests

* test(main): mock electron app for ipc handlers

* Update src/renderer/src/pages/settings/FFprobeSection.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Apply suggestion from @coderabbitai[bot]

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* refactor(types): extract common Platform and Arch types to shared package

- Create packages/shared/types/system.ts with Platform and Arch type definitions
- Remove duplicate type definitions from FFmpegDownloadService
- Remove duplicate type definitions from UvBootstrapperService
- Import shared types from @shared/types/system in both services

This change improves code maintainability by centralizing common system type definitions.

* fix(media-server): clear port and notify frontend on process exit

Ensure media server port is properly cleared and frontend is notified
when the process stops or crashes to prevent stale port usage.

Changes:
- Update notifyPortChanged signature to accept number | null
- Clear this.port to null in both normal stop and crash exit handlers
- Notify frontend with null port when server stops
- Update SessionService listener to handle null port notifications
- Add appropriate logging for port clearing events

This prevents the frontend from continuing to use expired ports after
the media server has stopped, requiring manual app restart.

* fix(settings): prevent setTimeout memory leak in MediaServerSection

Fix potential memory leak and unmounted component state update in
MediaServerSection by properly managing setTimeout lifecycle:

- Add successTimeoutRef to store timeout handle
- Clear timeout in effect cleanup on component unmount
- Clear timeout before creating new one in handleInstall
- Clear timeout in error handler to prevent orphaned timers
- Set ref to null after clearing to maintain clean state

This prevents "Can't perform a React state update on an unmounted
component" warnings and ensures proper cleanup of async operations.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(media-server): improve status display during installation and startup

- Add 'starting' status immediately after installation completes
- Move fetchServerInfo() to finally block to ensure status refresh
- Prevent confusing 'stopped' status display during startup process

* fix(python-venv): preserve final install progress state for callers

Remove finally block that immediately clears installProgress, preventing callers from reading final "completed" or "error" state. Instead:
- Clear installProgress at start of new installation
- Preserve final state after completion/error for caller consumption
- Remove unnecessary finally block cleanup

This ensures MediaServerSection and other callers can reliably read the final installation state.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(types): centralize media server types to shared package

- Create shared types module for Media Server and Python Venv
- Add media-server.ts with MediaServerInfo, MediaServerStatus, PythonVenvInfo, and InstallProgress types
- Remove duplicate type definitions from MediaServerService, PythonVenvService, and MediaServerSection
- Update imports to use @shared/types across main and renderer processes
- Maintain backward compatibility by re-exporting types from service files
- All type checks passing
github-actions bot pushed a commit that referenced this pull request Oct 15, 2025
# [1.1.0-beta.1](v1.0.0...v1.1.0-beta.1) (2025-10-15)

### Bug Fixes

* **AppUpdater, FFmpegDownloadService:** update default mirror source to global ([83194b3](83194b3))
* **build:** adjust resource handling for media-server in packaging ([086bd1b](086bd1b))
* **codec-compatibility:** handle missing codec information gracefully ([ea29f21](ea29f21))
* **FFmpegSection:** manage completion timeout for download process ([39b43c0](39b43c0))
* **FFprobeSection:** add return statement to download progress polling function ([49636cf](49636cf))
* **FFprobeSection:** ensure timeout cleanup after download success ([81a1431](81a1431))
* **FFprobeSection:** manage success timeout for download completion ([ce55d49](ce55d49))
* **FFprobeSection:** standardize font size using theme constants ([6387445](6387445))
* **FFprobeSection:** standardize spacing in styled components ([ba3c3d4](ba3c3d4))
* **homepage:** improve bottom spacing for card grid ([#194](#194)) ([801b6cd](801b6cd))
* make subtitle overlay container semantic ([2d6ae60](2d6ae60))
* **MediaServerService:** enhance error handling for file existence check ([11b74ef](11b74ef))
* **MediaServerService:** replace fs.existsSync with async stat for file existence check ([c9c98da](c9c98da))
* **player:** apply playback rate change through orchestrator when cycling speeds ([#210](#210)) ([fa9aa09](fa9aa09))
* **player:** remove HLS player missing error handling ([c7b593e](c7b593e))
* remove green glow effect from progress bar ([#196](#196)) ([abc6f3e](abc6f3e)), closes [#e50914](https://github.com/mkdir700/EchoPlayer/issues/e50914) [#00b96](https://github.com/mkdir700/EchoPlayer/issues/00b96)
* **semantic-release:** enhance version increment rules for prerelease branches ([#199](#199)) ([5d1e533](5d1e533))
* **theme:** resolve theme color not updating immediately for Switch components and progress bars ([#197](#197)) ([eed9ea2](eed9ea2))
* **TranscodeLoadingIndicator:** remove logging for loading indicator display ([085db44](085db44))
* **useSubtitleScrollStateMachine:** start auto-return timer on user interactions ([8496ae0](8496ae0))
* **UvBootstrapperService:** enhance UV download logic with cached path checks ([fc0791a](fc0791a))
* **UvBootstrapperService:** ensure temp directory cleanup after download ([02c7b16](02c7b16))
* **UvBootstrapperService:** prevent concurrent downloads by checking download controllers ([19d31e7](19d31e7))
* **VolumeIndicator:** skip indicator display on initial render ([82d2281](82d2281))
* **workflow:** update artifact listing command for better compatibility ([dfb6ee4](dfb6ee4))

### Features

* integrate session-backed HLS playback flow ([#200](#200)) ([ee972d1](ee972d1))
* intro backend for hls player ([2d34e7b](2d34e7b))
* **media-server:** add transcode cache cleanup for deleted videos ([e2de9ad](e2de9ad))
* **media-server:** implement runtime runtime management system ([#204](#204)) ([f5f68b0](f5f68b0))
* optimize media-server build output to resources directory ([#201](#201)) ([1b8c28e](1b8c28e))
* **player:** add animated loading progress bar to PlayerPage ([#206](#206)) ([8ba6f7f](8ba6f7f))
* **player:** add media server recommendation prompt for incompatible videos ([#205](#205)) ([63221a2](63221a2))
* **player:** add subtitle search functionality ([c3228c3](c3228c3))
* **player:** add toggle auto-pause functionality ([98b59ef](98b59ef))
* **player:** HLS session progress polling with media server integration ([#209](#209)) ([a76e8c2](a76e8c2))
* **PlayerSettingsLoader:** add mask mode to subtitle overlay ([56e4f65](56e4f65))
* **player:** update seek button icons from rewind/fastforward to undo/redo ([#193](#193)) ([1612c43](1612c43))
* **RegionDetection:** integrate region detection service for IP-based country identification ([dbeb077](dbeb077))
* **subtitle:** introduce mask mode for subtitle overlay ([e1fb3eb](e1fb3eb))
* **SubtitleOverlay:** enhance positioning and collision handling ([92b061a](92b061a))
* **UvBootstrapperService:** enhance download management with concurrency control ([20522e9](20522e9))

### Reverts

* "fix(build): adjust resource handling for media-server in packaging" ([2133401](2133401))
github-actions bot pushed a commit that referenced this pull request Oct 16, 2025
# [1.1.0](v1.0.0...v1.1.0) (2025-10-16)

### Bug Fixes

* **AppUpdater, FFmpegDownloadService:** update default mirror source to global ([83194b3](83194b3))
* **build:** adjust resource handling for media-server in packaging ([086bd1b](086bd1b))
* **codec-compatibility:** handle missing codec information gracefully ([ea29f21](ea29f21))
* **FFmpegSection:** manage completion timeout for download process ([39b43c0](39b43c0))
* **FFprobeSection:** add return statement to download progress polling function ([49636cf](49636cf))
* **FFprobeSection:** ensure timeout cleanup after download success ([81a1431](81a1431))
* **FFprobeSection:** manage success timeout for download completion ([ce55d49](ce55d49))
* **FFprobeSection:** standardize font size using theme constants ([6387445](6387445))
* **FFprobeSection:** standardize spacing in styled components ([ba3c3d4](ba3c3d4))
* **homepage:** improve bottom spacing for card grid ([#194](#194)) ([801b6cd](801b6cd))
* make subtitle overlay container semantic ([2d6ae60](2d6ae60))
* **MediaServerService:** enhance error handling for file existence check ([11b74ef](11b74ef))
* **MediaServerService:** replace fs.existsSync with async stat for file existence check ([c9c98da](c9c98da))
* **player:** apply playback rate change through orchestrator when cycling speeds ([#210](#210)) ([fa9aa09](fa9aa09))
* **player:** remove HLS player missing error handling ([c7b593e](c7b593e))
* **player:** restore muted state when re-entering player page ([1ec5c56](1ec5c56))
* remove green glow effect from progress bar ([#196](#196)) ([abc6f3e](abc6f3e)), closes [#e50914](https://github.com/mkdir700/EchoPlayer/issues/e50914) [#00b96](https://github.com/mkdir700/EchoPlayer/issues/00b96)
* **semantic-release:** enhance version increment rules for prerelease branches ([#199](#199)) ([5d1e533](5d1e533))
* **theme:** resolve theme color not updating immediately for Switch components and progress bars ([#197](#197)) ([eed9ea2](eed9ea2))
* **TranscodeLoadingIndicator:** remove logging for loading indicator display ([085db44](085db44))
* **useSubtitleScrollStateMachine:** start auto-return timer on user interactions ([8496ae0](8496ae0))
* **UvBootstrapperService:** enhance UV download logic with cached path checks ([fc0791a](fc0791a))
* **UvBootstrapperService:** ensure temp directory cleanup after download ([02c7b16](02c7b16))
* **UvBootstrapperService:** prevent concurrent downloads by checking download controllers ([19d31e7](19d31e7))
* **VolumeIndicator:** skip indicator display on initial render ([82d2281](82d2281))
* **workflow:** update artifact listing command for better compatibility ([dfb6ee4](dfb6ee4))

### Features

* integrate session-backed HLS playback flow ([#200](#200)) ([ee972d1](ee972d1))
* intro backend for hls player ([2d34e7b](2d34e7b))
* **media-server:** add transcode cache cleanup for deleted videos ([e2de9ad](e2de9ad))
* **media-server:** implement runtime runtime management system ([#204](#204)) ([f5f68b0](f5f68b0))
* optimize media-server build output to resources directory ([#201](#201)) ([1b8c28e](1b8c28e))
* **player:** add animated loading progress bar to PlayerPage ([#206](#206)) ([8ba6f7f](8ba6f7f))
* **player:** add media server recommendation prompt for incompatible videos ([#205](#205)) ([63221a2](63221a2))
* **player:** add subtitle search functionality ([c3228c3](c3228c3))
* **player:** add toggle auto-pause functionality ([98b59ef](98b59ef))
* **player:** HLS session progress polling with media server integration ([#209](#209)) ([a76e8c2](a76e8c2))
* **PlayerSettingsLoader:** add mask mode to subtitle overlay ([56e4f65](56e4f65))
* **player:** update seek button icons from rewind/fastforward to undo/redo ([#193](#193)) ([1612c43](1612c43))
* **RegionDetection:** integrate region detection service for IP-based country identification ([dbeb077](dbeb077))
* **subtitle:** introduce mask mode for subtitle overlay ([e1fb3eb](e1fb3eb))
* **SubtitleOverlay:** enhance positioning and collision handling ([92b061a](92b061a))
* **UvBootstrapperService:** enhance download management with concurrency control ([20522e9](20522e9))

### Reverts

* "fix(build): adjust resource handling for media-server in packaging" ([2133401](2133401))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant