档位改成「自动·第二高 + 实际档位」的下拉;手动合并也推送;头像缓存挪到程序目录
三件事: 1) 主播列表的录制档位 - 一律是下拉,不再有手填输入框。第一项「自动 · 第二高」(值 0)= 按这次读到的 档位排序自上往下取第二高(只有一档就用它,读不到档位表才退回最高档); 后面依次列出实际档位。选最高/最低/中间某一档直接点它,手动选过之后 「自动」仍留在列表里,随时能改回去。 - 语义变化:0 不再被写死成某个具体码率。原来记录档位时会把「没选过」的主播 按第二档定下来(写进 config.json),现在只在取流时解析,所以主播换了档位表 也能跟着走;app 启动时的补齐逻辑(applyCachedDefaultQualities)随之去掉。 - 解析规则统一到 resolveQualityBitrate:0 = 第二高,>0 = 不超过它的最高档, 都比它高时退回最低档;「自动」项会把解析到的档位名带出来 (自动 · 第二高(当前 = 蓝光 4M))。/api/status 和档位接口都多了 qualityBitrate。 - 点「档位」按钮时先给一句「正在读取码率档位…」:主播没开播时服务端要重试三次 才放弃,约 5 秒,之前这段时间界面没有任何反应。 2) 手动合并完成也推送(这就是「MeoW 只在测试时成功」的原因) - 原来只有自动合并调 sendNotify,手动合并从头到尾没推过,所以事件开着、渠道也通 却一直收不到。现在手动合并完成后同样按 mergeDone 事件推送两个渠道。 - 手动合并没有 uid,会拿成片名(再退到视频名)去对已有主播名,只有一个能对上时 才在文案里带上主播名,对不上就用「手动合并完成」。 3) 头像缓存挪到程序所在目录 - 原来存在「录播目录/avatar_cache」,换一次保存目录(甚至换盘)就要把同一批观众 的头像重新下一遍(一个头像可能 0.5MB)。现在统一放程序目录下的 avatar_cache。 - 旧位置作为只读回退:命中就搬过来(同盘改名,跨盘退回复制),不重复下载; 没被用到的旧文件留在原地,用到哪个搬哪个。 验证: - 新增 avatar_test.go 两个用例(缓存路径、旧位置搬家)和 notify_test.go 的 TestManualMergeSendsMergeDoneNotify(真跑一次合并,断言 webhook 与 MeoW 各收到 一条 mergeDone,且从文件名认出主播);qualities_test.go 改成新语义,新增 resolveQualityBitrate(7 种档位/码率组合)和 guessUIDByFileName。 - 端到端:把旧位置的头像缓存放在录播目录、头像地址指向连不通的端口,合并仍然成功 且画面里有那张头像(纯红像素 400 个),证明是搬来的而不是重新下载的;文件也从 录播目录搬到了程序目录。 - 浏览器:verify_streamer_br.py 重写成档位下拉的 20 项断言,verify_qualities.py 更新为新语义,两个都全过;verify_settings.py / verify_pick.py / verify_danmu_match.py 无回归;go test ./...、gofmt、go vet 干净。
This commit is contained in:
+102
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -592,3 +593,104 @@ func TestNotifyTestHandler(t *testing.T) {
|
||||
t.Errorf("非法昵称应该 400,实际 %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
// 手动合并完成也要推送 —— 以前只有自动合并会推,所以在页面上手动合并时 MeoW 一直没动静
|
||||
func TestManualMergeSendsMergeDoneNotify(t *testing.T) {
|
||||
setupMergeTest(t)
|
||||
ffmpeg := getFFmpeg()
|
||||
if ffmpeg == "" {
|
||||
t.Skip("没有 ffmpeg")
|
||||
}
|
||||
|
||||
savedConfig := config
|
||||
defer func() { config = savedConfig }()
|
||||
|
||||
var mu sync.Mutex
|
||||
var hooks []notifyPayload
|
||||
webhook := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var p notifyPayload
|
||||
_ = json.NewDecoder(r.Body).Decode(&p)
|
||||
mu.Lock()
|
||||
hooks = append(hooks, p)
|
||||
mu.Unlock()
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}))
|
||||
defer webhook.Close()
|
||||
|
||||
var meowQuery []string
|
||||
withMeowServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
meowQuery = append(meowQuery, r.URL.RawQuery)
|
||||
mu.Unlock()
|
||||
_, _ = w.Write([]byte(`{"status":200,"message":"推送成功"}`))
|
||||
})
|
||||
|
||||
// 只开合并完成这一个事件,两个渠道都开
|
||||
config.NotifyURL = webhook.URL
|
||||
config.NotifyLiveOn = false
|
||||
config.NotifyMergeDone = true
|
||||
config.MeowEnabled = true
|
||||
config.MeowNickname = "小小怪"
|
||||
config.MeowMsgType = "text"
|
||||
config.UIDs = uidList{{UID: 999, Danmu: true, Record: true}}
|
||||
// 文件名里带主播名,手动合并靠它认人
|
||||
streamers.Lock()
|
||||
streamers.crt = map[int]streamer{999: {UID: 999, Name: "测试主播"}}
|
||||
streamers.Unlock()
|
||||
|
||||
name := "2026-09-25 01-00-00 测试主播 标题"
|
||||
video := filepath.Join(recordSaveDir(), name+".mp4")
|
||||
if out, err := exec.Command(ffmpeg, "-v", "error", "-y",
|
||||
"-f", "lavfi", "-i", "color=c=black:size=320x180:rate=15", "-t", "3",
|
||||
"-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p", video).CombinedOutput(); err != nil {
|
||||
t.Fatalf("生成测试视频失败:%v\n%s", err, out)
|
||||
}
|
||||
lines := []string{`{"playResX":320,"playResY":180,"fontSize":16,"startTime":0}`,
|
||||
`{"t":0.5,"uid":101,"name":"甲","content":"手动合并推送测试"}`,
|
||||
`{"t":1.5,"uid":102,"name":"乙","content":"第二条"}`}
|
||||
writeTestFile(t, filepath.Join(recordSaveDir(), name+".jsonl"), strings.Join(lines, "\n")+"\n")
|
||||
|
||||
plan, err := buildMergePlan(mergeRequest{Videos: []string{name + ".mp4"}, Danmu: name + ".jsonl"})
|
||||
if err != nil {
|
||||
t.Fatalf("检查失败:%v", err)
|
||||
}
|
||||
if err := runManualMerge(plan); err != nil {
|
||||
t.Fatalf("手动合并失败:%v", err)
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
mu.Lock()
|
||||
nh, nm := len(hooks), len(meowQuery)
|
||||
mu.Unlock()
|
||||
if nh > 0 && nm > 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if len(hooks) != 1 {
|
||||
t.Fatalf("QQ 机器人应收到 1 条推送,实际 %d 条", len(hooks))
|
||||
}
|
||||
p := hooks[0]
|
||||
if p.Event != notifyEventMergeDone {
|
||||
t.Errorf("事件名应为 %s,实际 %s", notifyEventMergeDone, p.Event)
|
||||
}
|
||||
if p.UID != 999 || p.Name != "测试主播" {
|
||||
t.Errorf("应从文件名认出主播 999/测试主播,实际 %d/%q", p.UID, p.Name)
|
||||
}
|
||||
if p.File != plan.out {
|
||||
t.Errorf("推送里的文件应为成片 %q,实际 %q", plan.out, p.File)
|
||||
}
|
||||
if !strings.Contains(p.Text, "手动合并") || !strings.Contains(p.Text, plan.out) {
|
||||
t.Errorf("推送文案不对:%q", p.Text)
|
||||
}
|
||||
if len(meowQuery) != 1 {
|
||||
t.Fatalf("MeoW 应收到 1 条推送,实际 %d 条", len(meowQuery))
|
||||
}
|
||||
if !strings.Contains(meowQuery[0], "msgType=text") {
|
||||
t.Errorf("MeoW 的 msgType 应走 query 参数,实际 %q", meowQuery[0])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user