fix: 支持同目录 ffprobe 并在启动时检查 (#34)
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user