fix: 支持同目录 ffprobe 并在启动时检查 (#34)

This commit is contained in:
QiuSW
2026-10-06 14:19:12 +08:00
parent 2e0bf6e045
commit f9994885d0
6 changed files with 95 additions and 10 deletions
+16
View File
@@ -79,6 +79,11 @@ func (a *App) startup(ctx context.Context) {
})
a.log.Info("cmsp 启动")
go func() {
if err := downloader.CheckFFProbe(); err != nil {
a.log.Warn("视频校验工具不可用:%v", err)
}
}()
cfg, err := config.Load(a.cfgPath)
if err != nil {
@@ -130,6 +135,11 @@ func (a *App) GetConfig() config.Config {
return a.cfg
}
// CheckFFProbe 供启动界面显示视频校验工具是否就绪。
func (a *App) CheckFFProbe() error {
return downloader.CheckFFProbe()
}
// SaveConfig 校验并保存配置。
//
// 返回的 error 会变成前端的 Promise reject,界面直接把消息弹出来,
@@ -386,6 +396,9 @@ type UploadPreview struct {
// FetchVideosForProduct 为一个商品依次搜同款、提取视频并下载到本地。
// 单商品入口与批量任务共用 prepareVideoFetch;这里保持原有串行下载行为。
func (a *App) FetchVideosForProduct(productID string) (FetchResult, error) {
if err := downloader.CheckFFProbe(); err != nil {
return FetchResult{ProductID: productID}, err
}
ctx := a.appContext()
riskGuard := task.NewEmptyRiskGuard(a.cfg.Download.RiskEmptyThreshold)
result, work, err := a.prepareVideoFetch(ctx, productID, riskGuard, make(taobao.DetailCache), &taobao.LoginGuard{})
@@ -869,6 +882,9 @@ func (a *App) StartVideoTask(productIDs []string) error {
if len(productIDs) == 0 {
return fmt.Errorf("请先勾选要处理的商品")
}
if err := downloader.CheckFFProbe(); err != nil {
return err
}
a.videoTaskMu.Lock()
defer a.videoTaskMu.Unlock()
+5 -5
View File
@@ -8,8 +8,8 @@ rem
rem ============================================================
rem cmsp 打包脚本
rem
rem 双击本文件,把程序打包成一个 exe,可以直接发给同事使用。
rem 产物在 build\bin\cmsp.exe。
rem Copy cmsp.exe and ffprobe.exe together to the target computer.
rem Build output: build\bin\cmsp.exe.
rem
rem 只是想改代码看效果,请双击 run_dev.bat,不要用这个。
rem 打包比开发模式慢很多。
@@ -172,11 +172,11 @@ echo ============================================================
echo 打包成功
echo ============================================================
echo.
echo 产物:%CD%\build\bin\cmsp.exe
echo Build output: %CD%\build\bin\cmsp.exe
for %%f in ("build\bin\cmsp.exe") do echo 大小:%%~zf 字节
echo.
echo 发给同事时的注意事项:
echo - 只发 cmsp.exe 就够了,它已经把界面打包进去了
echo - Copy cmsp.exe and ffprobe.exe into the same folder.
echo - [必须] 不要把 config.yaml 一起发出去,里面有账号密码
echo - [必须] 不要把 cmsp.db 一起发出去,里面有登录状态
echo - 对方第一次打开后,到「参数设置」页填自己的账号
@@ -185,7 +185,7 @@ echo
echo - Microsoft Edge WebView2 运行时
echo ^(Win10 一般自带;没有的话程序会提示去装^)
echo - Google Chrome ^(用来登录淘宝^)
echo - ffprobe ^(用来校验下载的视频是否完整^)
echo - ffprobe.exe in the same folder as cmsp.exe ^(no PATH setup needed^)
echo.
rem 打开产物所在目录,省得手动找。
@@ -2,12 +2,16 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Local-Development-and-Verification
wiki_url: https://git.ilapage.cn/chengma/cmsp/wiki/Local-Development-and-Verification.-
wiki_revision: ebd03ea3ee41b327b1ed7dd4070f918a586cf4f2
synchronized_at: 2026-10-05T10:01:41Z
wiki_revision: 71370ea41be2c2da57519d75c4da44f9dbe81ce5
synchronized_at: 2026-10-06T06:15:48Z
<!-- gitea-wiki-mirror:end -->
# 本地开发与验证
## 跨电脑运行的 ffprobe(2026-10-06)
下载与上传前的视频完整性校验需要 `ffprobe.exe`。将打包后的 `cmsp.exe` 与 `ffprobe.exe` 复制到目标电脑的**同一目录**,无需在目标电脑配置 PATH。程序优先使用同目录文件;开发环境未放同目录文件时才查找 PATH。启动时若缺失或无法运行,界面与运行日志会提示;批量取视频在开始前也会检查,避免逐商品下载后才失败。`build.bat` 只生成 cmsp.exe,不会自动复制 ffprobe.exe;交付时须一并复制,不要复制本机配置、数据库或浏览器 Profile。
## 批量上传并发与选择范围(2026-10-05)
`upload.concurrency` 在本机 `config.yaml` 中配置,默认 1、允许 1—4;`config.example.yaml` 给出示例,参数设置页可修改并保存。旧配置缺字段保持串行;该值与仅控制 CDN 文件下载的 `download.concurrency` 独立。`app_upload.go` 对不同商品有界并行,单商品幂等键、未决操作只查原键、批次互斥及 `succeeded` 才写本地完成状态的规则不变。模拟 HTTP/临时 SQLite 的 `TestUploadBatchRespectsConfiguredConcurrency` 覆盖默认串行和并发 3,配置测试覆盖旧文件默认、保存读回与非法值;真实批量写入及 ERPGo 限流需单独验收。
+14 -1
View File
@@ -5,13 +5,14 @@
* 页面切换用 v-show 而不是 v-if,这样从设置页切回商品列表时
* 表格不用重新查一遍数据库。
*/
import { ref } from 'vue'
import { onMounted, ref } from 'vue'
// 组件已在 main.js 里整体注册,这里只取中文语言包。
import { zhCN, dateZhCN } from 'naive-ui'
import Sidebar from './components/Sidebar.vue'
import LogWindow from './components/LogWindow.vue'
import ProductListView from './views/ProductListView.vue'
import SettingsView from './views/SettingsView.vue'
import { CheckFFProbe } from '../wailsjs/go/main/App'
// 'list' 或 'set'
const page = ref('list')
@@ -19,6 +20,15 @@ const collapsed = ref(false)
const showLogs = ref(false)
const taobaoOk = ref(false)
const ffprobeError = ref('')
onMounted(async () => {
try {
await CheckFFProbe()
} catch (err) {
ffprobeError.value = String(err)
}
})
</script>
<template>
@@ -35,6 +45,9 @@ const taobaoOk = ref(false)
/>
<main class="content">
<n-alert v-if="ffprobeError" type="warning" closable @close="ffprobeError = ''">
视频校验不可用,下载和上传受影响:{{ ffprobeError }}
</n-alert>
<ProductListView v-show="page === 'list'" @open-logs="showLogs = true" />
<SettingsView v-show="page === 'set'" />
</main>
+34 -2
View File
@@ -211,9 +211,9 @@ func (d *Downloader) downloadOnce(ctx context.Context, sourceURL, referer, targe
}
func runFFProbe(ctx context.Context, path string) (ProbeResult, error) {
ffprobe, err := exec.LookPath("ffprobe")
ffprobe, err := findFFProbe()
if err != nil {
return ProbeResult{}, fmt.Errorf("未找到 ffprobe,请先安装并加入 PATH")
return ProbeResult{}, fmt.Errorf("未找到 ffprobe.exe,请将其放在 cmsp.exe 同目录,或安装后加入 PATH")
}
command := exec.CommandContext(ctx, ffprobe,
"-v", "error", "-select_streams", "v:0",
@@ -255,6 +255,38 @@ func runFFProbe(ctx context.Context, path string) (ProbeResult, error) {
Width: payload.Streams[0].Width, Height: payload.Streams[0].Height}, nil
}
func findFFProbe() (string, error) {
executable, err := os.Executable()
if err == nil {
return findFFProbeFor(executable)
}
return exec.LookPath("ffprobe")
}
func findFFProbeFor(executable string) (string, error) {
companion := filepath.Join(filepath.Dir(executable), "ffprobe.exe")
if info, err := os.Stat(companion); err == nil && !info.IsDir() {
return companion, nil
}
return exec.LookPath("ffprobe")
}
// CheckFFProbe 在开始任务前验证依赖可执行,避免下载后才发现缺失。
func CheckFFProbe() error {
path, err := findFFProbe()
if err != nil {
return fmt.Errorf("未找到 ffprobe.exe,请将其与 cmsp.exe 放在同一目录")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, path, "-version")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: windows.CREATE_NO_WINDOW}
if err := cmd.Run(); err != nil {
return fmt.Errorf("ffprobe.exe 无法运行,请检查同目录文件:%w", err)
}
return nil
}
// Probe 使用 ffprobe 读取本地视频的容器、时长、大小和首个视频流像素。
// 保留这个导出入口,使上传前预检和下载完整性校验复用同一份 ffprobe 行为。
func Probe(ctx context.Context, path string) (ProbeResult, error) { return runFFProbe(ctx, path) }
+20
View File
@@ -39,6 +39,26 @@ func Test文件名以蝦皮商品ID为前缀(t *testing.T) {
}
}
func Test优先使用程序同目录的FFProbe(t *testing.T) {
dir := t.TempDir()
companion := filepath.Join(dir, "ffprobe.exe")
if err := os.WriteFile(companion, []byte("test executable"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", "")
got, err := findFFProbeFor(filepath.Join(dir, "cmsp.exe"))
if err != nil || got != companion {
t.Fatalf("应优先找到同目录 ffprobe.exe:path=%q err=%v", got, err)
}
}
func Test程序同目录和PATH都缺少FFProbe(t *testing.T) {
t.Setenv("PATH", "")
if _, err := findFFProbeFor(filepath.Join(t.TempDir(), "cmsp.exe")); err == nil {
t.Fatal("缺少 ffprobe.exe 时必须返回错误")
}
}
func Test已存在非空文件直接跳过下载(t *testing.T) {
var requests atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {