Files
acfunlive-remake/output_test.go
T
tamakyi e35a4f7cef 合并先在本地做完再落到成片目录;头像支持 gif 和静态 webp
起因是核查合并落盘和头像处理时发现的两处问题:

1) 成片落盘:ffmpeg 原来是直接写目标目录(automerge.go 里那句注释「不再事后再
   移动一次」)。成片目录可能是网络盘或外接盘,写到一半失败会在那里留下一个
   看起来正常的半成品。现在改成先本地合成、完成后再落盘:
   - 输出先写到弹幕文件所在目录(本地)的 acfunlive_chunk_merging_*
   - 写完后由 placeOutput 落盘:同盘直接 os.Rename(瞬时),跨盘退回拷贝;
     拷贝中途失败留下的是 *.acfunlive.part,一眼看得出不是成品
   - 成功或失败都会清掉本地临时文件,失败时两个目录都不留东西
   - 临时文件沿用 chunkTempPrefix,不再出现在文件管理的列表里(listFiles 过滤)
   - 启动时清理上次残留;删不掉时提示一句 —— 强杀程序不会连带杀掉 ffmpeg
     子进程,它可能还在写这个文件,这种情况下次启动会再清一次

2) 头像:downloadAvatar 只注册了 jpeg 和 png 解码器,AcFun 上的 gif 头像必然
   解码失败,静态 webp 同样。加上 _ "image/gif"(多帧取第一帧)和
   _ "golang.org/x/image/webp"(x/image 本来就是直接依赖,零新增模块)。
   动画 webp 两条路都解不开(x/image 报 invalid format,ffmpeg 5.0 也不行),
   保持只烧文字。解码失败时错误信息带上 Content-Type,方便排查是哪张图。

在隔离实例上用真 ffmpeg 跑了真合并验证:gif 头像和 webp 头像都进了缓存并烧进
画面(抽帧确认圆形头像);日志里 ffmpeg 的输出是 record 下的临时文件,完成后
才出现在 merged;坏视频合成失败后成片目录没有半成品、录播目录零残留;合并
中途强杀留下的临时文件在下次启动时被清掉。

新增 output_test.go(7 个)和 avatar_test.go(3 个);go test ./...、gofmt、
go vet 干净。
2026-09-25 17:34:31 +08:00

138 lines
4.9 KiB
Go

package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
// 合成好的本地临时文件落到成片目录:同盘走改名,本地不该再留文件
func TestPlaceOutputMovesFile(t *testing.T) {
setupMergeTest(t)
tmp := filepath.Join(recordSaveDir(), chunkTempPrefix+"merging_测试_弹幕合并.mp4")
writeTestFile(t, tmp, "成片内容")
out := filepath.Join(mergeSaveDir(), "测试_弹幕合并.mp4")
if err := placeOutput(tmp, out); err != nil {
t.Fatalf("落盘失败:%v", err)
}
got, err := os.ReadFile(out)
if err != nil || string(got) != "成片内容" {
t.Fatalf("成片内容不对:%q %v", got, err)
}
if _, err := os.Stat(tmp); !os.IsNotExist(err) {
t.Error("落盘之后本地临时文件应该已经不在了")
}
}
// 成片目录里的子目录不存在时自动创建,已有同名成片时直接覆盖
func TestPlaceOutputCreatesDirAndOverwrites(t *testing.T) {
setupMergeTest(t)
sub := filepath.Join(mergeSaveDir(), "按日期", "2026-09")
if err := os.MkdirAll(sub, 0755); err != nil {
t.Fatal(err)
}
out := filepath.Join(sub, "同名_弹幕合并.mp4")
writeTestFile(t, out, "上一次的成片")
tmp := filepath.Join(recordSaveDir(), chunkTempPrefix+"merging_同名_弹幕合并.mp4")
writeTestFile(t, tmp, "这一次的成片")
if err := placeOutput(tmp, out); err != nil {
t.Fatalf("落盘失败:%v", err)
}
got, err := os.ReadFile(out)
if err != nil || string(got) != "这一次的成片" {
t.Fatalf("应该覆盖成新内容,实际:%q %v", got, err)
}
// 成片目录里不该留 .part
entries, _ := os.ReadDir(sub)
for _, e := range entries {
if strings.HasSuffix(e.Name(), partSuffix) {
t.Errorf("不该留下 %s", e.Name())
}
}
}
// 本地临时文件不存在时报错,而且不在成片目录留下任何东西
func TestPlaceOutputMissingSourceLeavesNothing(t *testing.T) {
setupMergeTest(t)
out := filepath.Join(mergeSaveDir(), "没有源文件_弹幕合并.mp4")
if err := placeOutput(filepath.Join(recordSaveDir(), "不存在.mp4"), out); err == nil {
t.Fatal("源文件不存在时应该报错")
}
entries, err := os.ReadDir(mergeSaveDir())
if err != nil {
t.Fatal(err)
}
for _, e := range entries {
if e.Name() == filepath.Base(out) || strings.HasSuffix(e.Name(), partSuffix) {
t.Errorf("失败之后不该留下 %s", e.Name())
}
}
}
// 临时成片放在弹幕文件所在目录(本地),且带临时前缀(否则会出现在文件管理里)
func TestTempMergeOutputPath(t *testing.T) {
dir := t.TempDir()
danmu := filepath.Join(dir, "record", "2026-09-25 01-00-00 甲.jsonl")
got := tempMergeOutput(danmu, "2026-09-25 01-00-00 甲_弹幕合并.mp4")
want := filepath.Join(dir, "record", chunkTempPrefix+"merging_2026-09-25 01-00-00 甲_弹幕合并.mp4")
if got != want {
t.Errorf("临时路径不对:\n got %s\nwant %s", got, want)
}
if !strings.HasPrefix(filepath.Base(got), chunkTempPrefix) {
t.Error("临时文件必须带临时前缀,否则会被当成正常文件列出")
}
}
// 启动时清理残留的临时文件,正常的录播 / 成片不动
func TestCleanTempFiles(t *testing.T) {
setupMergeTest(t)
staleRecord := []string{
filepath.Join(recordSaveDir(), chunkTempPrefix+"joined.mp4"),
filepath.Join(recordSaveDir(), chunkTempPrefix+"remapped.jsonl"),
filepath.Join(recordSaveDir(), chunkTempPrefix+"merging_甲_弹幕合并.mp4"),
}
staleMerge := filepath.Join(mergeSaveDir(), "甲_弹幕合并.mp4"+partSuffix)
for _, p := range append(append([]string{}, staleRecord...), staleMerge) {
writeTestFile(t, p, "残留")
}
keep := []string{
writeTestFile(t, filepath.Join(recordSaveDir(), "2026-09-25 01-00-00 甲.jsonl"), "弹幕"),
writeTestFile(t, filepath.Join(recordSaveDir(), "2026-09-25 01-00-00 甲.mp4"), "视频"),
writeTestFile(t, filepath.Join(mergeSaveDir(), "2026-09-25 01-00-00 甲_弹幕合并.mp4"), "成片"),
}
cleanTempFiles(recordSaveDir())
cleanTempFiles(mergeSaveDir())
for _, p := range append(append([]string{}, staleRecord...), staleMerge) {
if _, err := os.Stat(p); !os.IsNotExist(err) {
t.Errorf("残留的临时文件应该被清掉:%s", filepath.Base(p))
}
}
for _, p := range keep {
if _, err := os.Stat(p); err != nil {
t.Errorf("正常文件不该被删:%s", filepath.Base(p))
}
}
}
// 临时文件不出现在文件管理的列表里
func TestListFilesHidesTemp(t *testing.T) {
setupMergeTest(t)
writeTestFile(t, filepath.Join(recordSaveDir(), "2026-09-25 01-00-00 甲.mp4"), "视频")
writeTestFile(t, filepath.Join(recordSaveDir(), chunkTempPrefix+"merging_甲_弹幕合并.mp4"), "临时")
writeTestFile(t, filepath.Join(recordSaveDir(), "甲_弹幕合并.mp4"+partSuffix), "半成品")
names := make([]string, 0, 4)
for _, f := range listFiles(recordSaveDir()) {
names = append(names, f.Name)
}
if len(names) != 1 || names[0] != "2026-09-25 01-00-00 甲.mp4" {
t.Errorf("列表里应该只剩正常录播文件,实际:%v", names)
}
}