diff --git a/automerge.go b/automerge.go index bb4e46b..c625d04 100644 --- a/automerge.go +++ b/automerge.go @@ -85,6 +85,14 @@ func findMovedFile(path string) string { return "" } +// 收拢到实际位置:找不着时退回原路径,让后面的报错还能指出「本来想找哪儿」 +func resolveAfterMove(path string) string { + if p := findMovedFile(path); p != "" { + return p + } + return path +} + // 录播因为断流可能分成多段:单段直接使用,多段先拼成完整视频并把弹幕时间映射到拼接后的时间轴。 // 返回合并要用的视频、字幕文件和弹幕相对视频起点的时间偏移、**实际参与合并的源文件**, // 以及清理临时文件的函数。used 是给「合并完删源视频」用的:中途回退成「只合并第一个分段」时, @@ -92,6 +100,10 @@ func findMovedFile(path string) string { func prepareForMerge(ffmpegFile string, segs []recordSegment, jsonlFile string, danmuStartNs int64) (string, string, float64, []string, func(), error) { noop := func() {} + // 弹幕文件同样会被移动过去。以前只有视频分段走了查找,弹幕还拿着录制时的路径, + // 结果就是视频找得到、弹幕找不到,合并以「jsonl 文件不存在」告终 + jsonlFile = resolveAfterMove(jsonlFile) + // 录制结束时文件可能已经被移动到保存目录,逐个找实际位置 files := make([]string, 0, len(segs)) starts := make([]int64, 0, len(segs)) @@ -288,6 +300,10 @@ func autoMergeOutFile(jsonlFile string) string { func autoMergeJob(uid int, segs []recordSegment, jsonlFile string, danmuStartNs int64) error { lPrintln("开始自动合并" + longID(uid) + "的录播和弹幕层") + // 录制结束时弹幕文件可能已被移动到保存目录,先把路径收拢到实际位置: + // 中间成片写到哪、事后删哪几个弹幕文件都按这一条路径来,用旧路径会指向错误的地方 + jsonlFile = resolveAfterMove(jsonlFile) + // 视频和弹幕是一次录制产生的,文件名去掉扩展名后应该一致(扩展名由 recordFormat 决定) if len(segs) > 0 { videoBase := filepath.Base(strings.TrimSuffix(segs[0].file, filepath.Ext(segs[0].file))) diff --git a/automerge_test.go b/automerge_test.go new file mode 100644 index 0000000..7457b9c --- /dev/null +++ b/automerge_test.go @@ -0,0 +1,76 @@ +package main + +import ( + "os" + "path/filepath" + "testing" +) + +// 录制结束时文件会被移动到保存目录,视频分段和弹幕文件都要按新位置找。 +// 以前只有视频分段走了查找,弹幕还拿着录制时的路径:视频找到了、弹幕找不到, +// 整场合并以「jsonl 文件不存在」告终(用户看到的就是合成失败 + 一条失败推送)。 +func TestPrepareForMergeFindsMovedJsonl(t *testing.T) { + savedConfig := config + defer func() { config = savedConfig }() + + root := t.TempDir() + config.Directory = root + config.RecordSubdir = "record" + if err := os.MkdirAll(recordSaveDir(), 0755); err != nil { + t.Fatal(err) + } + + name := "2026-09-27 22-40-42 甲 标题" + // 录制时的位置:文件已经被搬走,这里不该再有 + oldDir := t.TempDir() + oldVideo := filepath.Join(oldDir, name+".mp4") + oldJsonl := filepath.Join(oldDir, name+".jsonl") + // 实际位置 + newVideo := filepath.Join(recordSaveDir(), name+".mp4") + newJsonl := filepath.Join(recordSaveDir(), name+".jsonl") + for _, p := range []string{newVideo, newJsonl} { + if err := os.WriteFile(p, []byte("{}\n"), 0644); err != nil { + t.Fatal(err) + } + } + + segs := []recordSegment{{file: oldVideo, startNs: 0}} + video, jsonl, _, _, cleanup, err := prepareForMerge("没有这个-ffmpeg.exe", segs, oldJsonl, 0) + defer cleanup() + if err != nil { + t.Fatalf("单段不该报错:%v", err) + } + if video != newVideo { + t.Errorf("视频应当按保存目录里的实际位置找:\n got %s\n want %s", video, newVideo) + } + if jsonl != newJsonl { + t.Errorf("弹幕文件应当按保存目录里的实际位置找:\n got %s\n want %s", jsonl, newJsonl) + } +} + +// 文件确实不在(既没在录制时的位置、也不在保存目录)时,要退回原路径而不是空串: +// 否则报错会变成「文件不存在:」,看不出到底缺的是哪个 +func TestResolveAfterMoveKeepsOriginalWhenMissing(t *testing.T) { + savedConfig := config + defer func() { config = savedConfig }() + + root := t.TempDir() + config.Directory = root + config.RecordSubdir = "record" + + gone := filepath.Join(t.TempDir(), "这个目录是对的但文件不在.mp4") + if got := resolveAfterMove(gone); got != gone { + t.Errorf("找不到时应当退回原路径:got %q want %q", got, gone) + } + + real := filepath.Join(root, "record", "在保存目录里.mp4") + if err := os.MkdirAll(filepath.Dir(real), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(real, []byte("x"), 0644); err != nil { + t.Fatal(err) + } + if got := resolveAfterMove(real); got != real { + t.Errorf("文件就在原路径时不该改:got %q want %q", got, real) + } +}