转码的源文件不再限死在录播/成片目录,可以填任意路径

为什么
  转码页的多选框只列 /api/files 的内容,也就是录播目录和成片目录两处;后端的
  resolveTranscodeInput 走 resolveFile,也只认 record/merge 两个目录名。结果是
  录播原片被合并且清掉之后(或者录播目录本来就空),转码页里只剩合并结果可挑,
  磁盘上别处的视频一个都进不来。

怎么改
  - transcodeJobReq 加 Path 字段,与 dir/name 二选一;新增 resolveTranscodeJob()
    把「填了路径就按路径找(认绝对路径和从资源管理器复制来的带引号路径,走
    resolveMergeInput)、否则回落 dir+name」收在一处,两条路最后统一过
    mergeVideoExts 白名单 —— 路径这条能指向磁盘上任何文件,不挡的话手滑把 .jsonl
    丢进来要等 ffmpeg 跑起来才报错。三个调用点和 runTranscode 全部改走它。
  - transcodeProbe 回显绝对路径,页面拿它当任务身份:同名文件在不同目录里不会再
    被认成一个,重复加入也因此挡得住。
  - 转码卡片改成两栏:左边保留原来的文件列表多选,右边加「一行一个路径」的输入框
    和「浏览…」按钮(复用已有的目录选择器,可多选、可跨文件夹跨盘攒,选完只追加不
    覆盖)。openPick 多了个可选的第 5 参数用来指定起始目录,浏览转码文件时从成片
    目录开始(录播目录可能是空的)。
  - 报错前缀改用 transcodeJobReq.label():按路径进来时 Name 是空的,直接拼 j.Name
    会变成「:参数不合法」。

验证
  gofmt -l 干净;go vet 干净;Windows 与 GOOS=linux 编译均通过。
  go test ./... 全绿;go test -race -count=1 ./... 27.2s 全绿;顶层用例 209 个
  (含子测试 226),本次新增 3 个:-run 单独跑过 TestResolveTranscodeJobByPath /
  TestResolveTranscodeJobRejectsBadPath / TestTranscodeProbeHandlerAcceptsPath。
  反向验证 44/44:把 resolveTranscodeJob 的路径分支退回 resolveFile 后,上面两个
  用例确实变红。
  转码接口冒烟 40/40(新增 11 项按路径的:探测回显路径、估算、不存在 400、非视频
  后缀 400、指向文件夹 400、真排队转出成片)。
  页面 jsdom 冒烟 49/49 且无 JS 报错(新增 12 项:路径框存在、空框不给加入、手填能
  进列表并带上绝对路径、探测请求按 path 发、重复加入不重复、浏览弹窗打开/追加不
  覆盖/关闭/清空)。
This commit is contained in:
tamakyi
2026-09-27 11:10:00 +08:00
parent a409c5b4dd
commit 3c17503a21
5 changed files with 214 additions and 39 deletions
+33 -11
View File
@@ -66,17 +66,29 @@ type transcodeOptions struct {
Audio string `json:"audio"` // 音频处理,空 = 直接拷贝
}
// 一个待转码的文件
// 一个待转码的文件。
// 定位源文件有两种写法,二选一:Path 填了就用它(绝对路径、相对路径都行,可以是从资源管理器
// 复制来的带引号路径),没填才按 Dir + Name 在录播/成片文件夹里找
type transcodeJobReq struct {
Path string `json:"path"`
Dir string `json:"dir"` // record / merge
Name string `json:"name"` // 文件名
OutName string `json:"outName"` // 输出文件名,留空自动
Options transcodeOptions `json:"options"`
}
// 报错时给用户看的名字:按路径传进来的 Name 是空的,直接拼进消息会变成「:参数不合法」
func (j transcodeJobReq) label() string {
if p := strings.TrimSpace(j.Path); p != "" {
return filepath.Base(strings.Trim(p, `"'`))
}
return j.Name
}
// 探测到的源文件信息,页面用它显示原始参数和算预估体积
type transcodeProbe struct {
Name string `json:"name"`
Path string `json:"path"` // 源文件绝对路径,页面拿它当任务的身份(同名文件在不同目录不会认错)
Dir string `json:"dir"`
Size int64 `json:"size"`
Duration float64 `json:"duration"`
@@ -531,9 +543,19 @@ func formatByteSize(n int64) string {
return fmt.Sprintf("%.1f %s", v, units[i])
}
// 转码任务的输入必须来自文件管理里的两个文件夹,和手动合并一致
func resolveTranscodeInput(dir, name string) (string, error) {
full, err := resolveFile(dir, name)
// 转码任务的输入:填了路径就按路径找(任意文件夹都行),否则按文件名在录播/成片文件夹里找。
// 两条路都要过一遍格式白名单 —— 路径这条能指向磁盘上任何文件,不挡的话
// 用户手滑把 .jsonl 弹幕丢进来,要等 ffmpeg 跑起来才报一句看不懂的错
func resolveTranscodeJob(j transcodeJobReq) (string, error) {
var (
full string
err error
)
if p := strings.TrimSpace(j.Path); p != "" {
full, err = resolveMergeInput(p)
} else {
full, err = resolveFile(j.Dir, j.Name)
}
if err != nil {
return "", err
}
@@ -650,7 +672,7 @@ func runTranscode(job transcodeJobReq, outDir string, idx, total int) (string, e
if ffmpegFile == "" {
return "", errors.New("找不到 ffmpeg,请先在设置里填写 ffmpeg 路径")
}
in, err := resolveTranscodeInput(job.Dir, job.Name)
in, err := resolveTranscodeJob(job)
if err != nil {
return "", err
}
@@ -803,7 +825,7 @@ func transcodeProbeHandler(w http.ResponseWriter, r *http.Request) {
out := make([]transcodeProbe, 0, len(req.Files))
for _, f := range req.Files {
full, err := resolveTranscodeInput(f.Dir, f.Name)
full, err := resolveTranscodeJob(f)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -815,7 +837,7 @@ func transcodeProbeHandler(w http.ResponseWriter, r *http.Request) {
}
m := probeMediaCached(ffmpegFile, full)
out = append(out, transcodeProbe{
Name: filepath.Base(full), Dir: f.Dir, Size: st.Size(),
Name: filepath.Base(full), Path: full, Dir: f.Dir, Size: st.Size(),
Duration: m.duration, Width: m.width, Height: m.height, Fps: m.fps,
// 视频码率在这里就扣掉音轨:后面预估体积还要单独加一次音频,
// 直接用容器总码率会把音轨算成两份
@@ -852,10 +874,10 @@ func transcodeEstimateHandler(w http.ResponseWriter, r *http.Request) {
out := make([]transcodeEstimate, 0, len(req.Jobs))
for _, j := range req.Jobs {
if msg := j.Options.validate(ffmpegFile); msg != "" {
http.Error(w, j.Name+":"+msg, http.StatusBadRequest)
http.Error(w, j.label()+":"+msg, http.StatusBadRequest)
return
}
full, err := resolveTranscodeInput(j.Dir, j.Name)
full, err := resolveTranscodeJob(j)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -907,10 +929,10 @@ func transcodeHandler(w http.ResponseWriter, r *http.Request) {
// 探测走缓存,刚才「加入列表」和「估算体积」时已经探过,这里不会再多起 ffmpeg
for _, j := range req.Jobs {
if msg := j.Options.validate(ffmpegFile); msg != "" {
http.Error(w, j.Name+":"+msg, http.StatusBadRequest)
http.Error(w, j.label()+":"+msg, http.StatusBadRequest)
return
}
full, err := resolveTranscodeInput(j.Dir, j.Name)
full, err := resolveTranscodeJob(j)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return