Files
acfunlive-remake/qualities_test.go
T
tamakyi 237fffa8fe 录制档位改成「最高/第二高/最低/自定义」四种相对偏好,没开播也能先设
上一版的下拉里列的是「这次读到的实际档位」,没读到档位表(没开播)时下拉里就只剩
一项,想预先设「我要最高档」或「最低档」都做不到。改成按「实际档位里的第几档」来选:

- 新增 `qualityPref`:top(最高档)/ second(第二高,默认)/ lowest(最低档)/
  custom(取不超过给定码率的最高档,都比它高时退回最低档)。四种偏好任何时候都
  在列表里,开播后自动对应到实际档位;读到档位表后把对应结果标在选项后面
  (`第二高(蓝光 4M)`、`自定义(≤2500K = 高清)`)。
- `bitrate` 只在 custom 时有意义。`resolveQualityBitrate(tiers, pref, custom)` 统一
  解析:相对偏好没有档位表时返回 0(取流退回最高档),custom 返回填的上限。
  解析只在取流时做,所以主播换了档位表,偏好会跟着一起变。
- 老配置兼容:没有 qualityPref 时,`bitrate > 0` 当成 custom(就是原来的「码率上限」,
  行为不变),`bitrate == 0` 当成默认的第二高;在 checkConfig 里补齐(不写盘,下次
  保存自然带上)。`setBitrate` 相应换成 `setQuality(uid, pref, custom)`。
- 页面:选 custom 时旁边出现码率框,初始值就是**当前实际在用的那一档**(不会一切过去
  就掉到最低档),回车或失焦即提交,填错给提示并回填服务端的值;从 custom 切到别的
  档位不带 bitrate,服务端保留原值,改回来还是原来那个数。
- 设置区同步:新主播的默认档位也用同一套四项(`defaultQualityPref`),只有选自定义
  才露出码率上限框(切过去会补一个能用的 3000,免得新主播默默用最低档)。

顺带修一个脚本抓到的真 bug:档位名传成了档位对象(`esc()` 里会 `.replace`),只要
档位表非空就会抛异常,而 refresh() 整段包在 try/catch 里 —— 表现是整个主播列表
变成空白且没有任何报错。现在取的是 `qs[i].name`。

测试:qualities_test.go 重写 TestResolveQualityBitrate(10 种偏好/码率组合)并新增
TestMigrateQualityPref、TestUpgradeQualityPrefs、TestSetQuality;verify_streamer_br.py
改成带迷你后端的假服务(POST 会真正落到假状态上),30 项断言全过;verify_qualities.py
25 项(含设置区与 config.json 往返)全过;verify_settings.py / verify_pick.py /
verify_danmu_match.py 无回归;go test ./...、gofmt、go vet 干净。
2026-09-25 20:42:22 +08:00

362 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func tiersForTest() []qualityTier {
// 故意乱序,验证排序
return []qualityTier{
{Name: "高清", Bitrate: 2000},
{Name: "蓝光 8M", Bitrate: 8000},
{Name: "标清", Bitrate: 1000},
{Name: "蓝光 4M", Bitrate: 4000},
}
}
func TestSortQualitiesDesc(t *testing.T) {
got := sortQualitiesDesc(tiersForTest())
want := []string{"蓝光 8M", "蓝光 4M", "高清", "标清"}
for i, name := range want {
if got[i].Name != name {
t.Fatalf("第 %d 个应为 %s,实际 %s(%v)", i, name, got[i].Name, got)
}
}
// 不能改动传进来的切片
in := tiersForTest()
_ = sortQualitiesDesc(in)
if in[0].Name != "高清" {
t.Errorf("排序不该改动原切片:%v", in)
}
}
// 默认档位 = 自上往下第二档(不是最高档)
func TestDefaultQualityBitrate(t *testing.T) {
cases := []struct {
name string
tiers []qualityTier
want int
}{
{"四档取第二高", tiersForTest(), 4000},
{"三档取第二高(你给的例子)", []qualityTier{{"高清", 2000}, {"蓝光 8M", 8000}, {"蓝光 4M", 4000}}, 4000},
{"两档取低的那档", []qualityTier{{"蓝光 4M", 4000}, {"高清", 2000}}, 2000},
{"只有一档就用它", []qualityTier{{"高清", 2000}}, 2000},
{"没有档位返回 0", nil, 0},
}
for _, c := range cases {
if got := defaultQualityBitrate(c.tiers); got != c.want {
t.Errorf("%s:应为 %d,实际 %d", c.name, c.want, got)
}
}
}
func TestQualityNameFor(t *testing.T) {
if got := qualityNameFor(tiersForTest(), 4000); got != "蓝光 4M" {
t.Errorf("4000 应匹配「蓝光 4M」,实际 %q", got)
}
if got := qualityNameFor(tiersForTest(), 9999); got != "" {
t.Errorf("匹配不上应返回空,实际 %q", got)
}
}
func TestSameQualities(t *testing.T) {
a := tiersForTest()
b := []qualityTier{{"标清", 1000}, {"蓝光 4M", 4000}, {"高清", 2000}, {"蓝光 8M", 8000}}
if !sameQualities(a, b) {
t.Error("顺序不同但内容相同的档位列表应视为一样")
}
c := append([]qualityTier(nil), a[:3]...)
if sameQualities(a, c) {
t.Error("档位数量不同应视为不一样")
}
}
// 记录档位时只缓存档位表,不动用户选的码率:
// 0 表示「自动、按实际档位取第二高」,主播换了档位表要能跟着走
func TestSetStreamerQualities(t *testing.T) {
savedConfig := config
savedStreamers := streamers.crt
defer func() {
config = savedConfig
streamers.crt = savedStreamers
}()
config.UIDs = uidList{
{UID: 1, Danmu: true, Record: true, Bitrate: 0}, // 自动
{UID: 2, Danmu: true, Record: true, Bitrate: 1000}, // 手动指定过
}
streamers.crt = map[int]streamer{
1: {UID: 1, Bitrate: 0},
2: {UID: 2, Bitrate: 1000},
}
if !setStreamerQualities(1, tiersForTest()) {
t.Error("第一次记录档位应该算改动了配置")
}
if config.UIDs[0].Bitrate != 0 {
t.Errorf("自动的主播不该被写死成具体码率,实际 %d", config.UIDs[0].Bitrate)
}
if len(config.UIDs[0].Qualities) != 4 || len(streamers.crt[1].Qualities) != 4 {
t.Errorf("档位没缓存到配置或运行时数据:%+v / %+v", config.UIDs[0], streamers.crt[1])
}
if streamers.crt[1].Bitrate != 0 {
t.Errorf("运行时数据的码率不该被改动,实际 %d", streamers.crt[1].Bitrate)
}
// 手动设过的同样不动
setStreamerQualities(2, tiersForTest())
if config.UIDs[1].Bitrate != 1000 {
t.Errorf("uid 2 手动设过 1000,不该被改成 %d", config.UIDs[1].Bitrate)
}
// 同样的档位再来一次不算改动
if setStreamerQualities(1, tiersForTest()) {
t.Error("档位没变化时不该算改动")
}
// 档位变了才算改动
if !setStreamerQualities(1, []qualityTier{{"高清", 2000}, {"流畅", 500}}) {
t.Error("档位变化时应算改动")
}
}
// 按偏好决定实际用哪个档位:最高/第二高/最低按实际档位排序取,
// 自定义取不超过给定码率的最高档、都比它高时退回最低档
func TestResolveQualityBitrate(t *testing.T) {
cases := []struct {
name string
pref string
custom int
want int
}{
{"最高档", qualityPrefTop, 0, 8000},
{"第二高(默认)", qualityPrefSecond, 0, 4000},
{"最低档", qualityPrefLowest, 0, 1000},
{"空偏好按默认的第二高", "", 0, 4000},
{"没见过的偏好也按第二高", "bogus", 0, 4000},
{"自定义取不超过上限的最高档", qualityPrefCustom, 3000, 2000},
{"自定义上限正好等于某一档", qualityPrefCustom, 4000, 4000},
{"自定义上限比所有档位都高就取最高档", qualityPrefCustom, 99999, 8000},
{"自定义上限比所有档位都低就退回最低档", qualityPrefCustom, 200, 1000},
{"自定义填 0 等于最低档", qualityPrefCustom, 0, 1000},
}
for _, c := range cases {
if got := resolveQualityBitrate(tiersForTest(), c.pref, c.custom); got != c.want {
t.Errorf("%s:应为 %d,实际 %d", c.name, c.want, got)
}
}
// 没有档位表时:相对偏好返回 0(取流时退回最高档),自定义返回填的上限
if got := resolveQualityBitrate(nil, qualityPrefSecond, 0); got != 0 {
t.Errorf("没有档位表时相对偏好应返回 0,实际 %d", got)
}
if got := resolveQualityBitrate(nil, qualityPrefCustom, 2500); got != 2500 {
t.Errorf("没有档位表时自定义应返回填的 2500,实际 %d", got)
}
// 只有一档时:最高/第二高都是它,最低也是它
one := []qualityTier{{"高清", 2000}}
for _, pref := range []string{qualityPrefTop, qualityPrefSecond, qualityPrefLowest} {
if got := resolveQualityBitrate(one, pref, 0); got != 2000 {
t.Errorf("只有一档时 %s 应为 2000,实际 %d", pref, got)
}
}
}
// 老配置只有 bitrate,读进来时要能推断出偏好:>0 是「自定义上限」,0 是默认的第二高
func TestMigrateQualityPref(t *testing.T) {
cases := []struct {
pref string
bitrate int
want string
}{
{"", 0, qualityPrefSecond},
{"", 5000, qualityPrefCustom},
{"", 1000, qualityPrefCustom},
{qualityPrefTop, 0, qualityPrefTop},
{qualityPrefLowest, 5000, qualityPrefLowest},
{qualityPrefCustom, 3000, qualityPrefCustom},
{" ", 0, qualityPrefSecond}, // 认不出来的值按默认
{"TOP", 0, qualityPrefSecond}, // 大小写不敏感不做,保持严格
}
for _, c := range cases {
if got := migrateQualityPref(c.pref, c.bitrate); got != c.want {
t.Errorf("pref=%q bitrate=%d 应为 %s,实际 %s", c.pref, c.bitrate, c.want, got)
}
}
// normalizeQualityPref 对四种合法值原样返回
for _, p := range []string{qualityPrefTop, qualityPrefSecond, qualityPrefLowest, qualityPrefCustom} {
if got := normalizeQualityPref(p); got != p {
t.Errorf("%s 应原样返回,实际 %s", p, got)
}
}
}
// 升级老配置:config.UIDs 和默认档位都要补上 qualityPref
func TestUpgradeQualityPrefs(t *testing.T) {
savedConfig := config
defer func() { config = savedConfig }()
config.UIDs = uidList{
{UID: 1}, // 老配置:bitrate 0 → 第二高
{UID: 2, Bitrate: 5000}, // 老配置:有码率 → 自定义
{UID: 3, QualityPref: qualityPrefTop}, // 已经是新配置 → 不动
}
config.DefaultQualityPref = ""
config.DefaultBitrate = 0
upgradeQualityPrefs()
if config.UIDs[0].QualityPref != qualityPrefSecond {
t.Errorf("uid 1 应补成 second,实际 %s", config.UIDs[0].QualityPref)
}
if config.UIDs[1].QualityPref != qualityPrefCustom {
t.Errorf("uid 2 应补成 custom,实际 %s", config.UIDs[1].QualityPref)
}
if config.UIDs[2].QualityPref != qualityPrefTop {
t.Errorf("uid 3 不该被改动,实际 %s", config.UIDs[2].QualityPref)
}
if config.DefaultQualityPref != qualityPrefSecond {
t.Errorf("默认档位应补成 second,实际 %s", config.DefaultQualityPref)
}
// 幂等
upgradeQualityPrefs()
if config.UIDs[1].QualityPref != qualityPrefCustom {
t.Error("重复升级不该改变结果")
}
}
// 改档位偏好:写配置 + 同步运行时数据
func TestSetQuality(t *testing.T) {
savedConfig := config
savedStreamers := streamers.crt
savedLocation := configFileLocation
oldDir := configDir
if configDir == nil {
configDir = new(string)
}
defer func() {
config = savedConfig
streamers.crt = savedStreamers
configFileLocation = savedLocation
configDir = oldDir
}()
*configDir = t.TempDir()
configFileLocation = filepath.Join(*configDir, configFile)
config.UIDs = uidList{{UID: 7, QualityPref: qualityPrefSecond}}
streamers.crt = map[int]streamer{7: {UID: 7, QualityPref: qualityPrefSecond}}
if !setQuality(7, qualityPrefTop, 0) {
t.Fatal("改档位应该成功")
}
if config.UIDs[0].QualityPref != qualityPrefTop || streamers.crt[7].QualityPref != qualityPrefTop {
t.Errorf("档位偏好没同步:%+v / %+v", config.UIDs[0], streamers.crt[7])
}
// 自定义时码率上限也要存下来
if !setQuality(7, qualityPrefCustom, 2500) {
t.Fatal("改成自定义应该成功")
}
if streamers.crt[7].Bitrate != 2500 || config.UIDs[0].Bitrate != 2500 {
t.Errorf("自定义码率没存:%+v / %+v", config.UIDs[0], streamers.crt[7])
}
// 认不出来的偏好按默认处理,负数码率直接拒绝
setQuality(7, "bogus", 2500)
if streamers.crt[7].QualityPref != qualityPrefSecond {
t.Errorf("非法偏好应按默认第二高,实际 %s", streamers.crt[7].QualityPref)
}
if setQuality(7, qualityPrefCustom, -1) {
t.Error("负数码率应该被拒绝")
}
if setQuality(999, qualityPrefTop, 0) {
t.Error("不在列表里的 uid 应该失败")
}
}
// 手动合并的通知靠文件名猜主播:只有一个名字对得上才认
func TestGuessUIDByFileName(t *testing.T) {
savedConfig := config
savedStreamers := streamers.crt
defer func() {
config = savedConfig
streamers.crt = savedStreamers
}()
config.UIDs = uidList{{UID: 1}, {UID: 2}, {UID: 3}}
streamers.crt = map[int]streamer{
1: {UID: 1, Name: "AC娘本体"},
2: {UID: 2, Name: "随便唠会"},
3: {UID: 3, Name: "阿"},
}
cases := []struct {
file string
want int
}{
{"2024-10-14 00-33-32 AC娘本体 嘿嘿 醒了_弹幕合并.mp4", 1},
{"AC娘本体.mp4", 1},
{"2024-10-14 00-33-32 随便唠会 摸鱼.mp4", 2},
// 两个名字都在文件名里,猜不准就不猜
{"AC娘本体 和 随便唠会 联动.mp4", 0},
{"完全不认识的主播.mp4", 0},
// 名字只有一个字的不参与匹配,太容易误伤
{"阿 的录播.mp4", 0},
{"", 0},
}
for _, c := range cases {
if got := guessUIDByFileName(c.file); got != c.want {
t.Errorf("%q 应猜出 %d,实际 %d", c.file, c.want, got)
}
}
}
// 档位要能存进 config.json 再读回来
func TestQualitiesConfigRoundTrip(t *testing.T) {
savedConfig := config
savedLocation := configFileLocation
oldDir := configDir
if configDir == nil {
configDir = new(string)
}
defer func() {
config = savedConfig
configFileLocation = savedLocation
configDir = oldDir
}()
dir := t.TempDir()
*configDir = dir
configFileLocation = filepath.Join(dir, configFile)
body := `{"uids":[{"uid":23682490,"danmu":true,"record":true,"bitrate":4000,
"qualities":[{"name":"蓝光 8M","bitrate":8000},{"name":"蓝光 4M","bitrate":4000}]}]}`
if err := os.WriteFile(configFileLocation, []byte(body), 0644); err != nil {
t.Fatal(err)
}
config = configData{}
loadConfig()
if len(config.UIDs) != 1 {
t.Fatalf("应解析出 1 个主播,实际 %d", len(config.UIDs))
}
u := config.UIDs[0]
if u.Bitrate != 4000 || len(u.Qualities) != 2 || u.Qualities[0].Name != "蓝光 8M" {
t.Fatalf("档位没正确读出来:%+v", u)
}
if got := qualityNameFor(u.Qualities, u.Bitrate); got != "蓝光 4M" {
t.Errorf("当前档位名应为「蓝光 4M」,实际 %q", got)
}
// 再序列化一次,字段名要对得上(qualities / name / bitrate)
config.UIDs[0].Qualities = sortQualitiesDesc(config.UIDs[0].Qualities)
data, err := json.Marshal(config.UIDs[0])
if err != nil {
t.Fatal(err)
}
for _, key := range []string{`"qualities"`, `"name"`, `"bitrate"`} {
if !json.Valid(data) || !strings.Contains(string(data), key) {
t.Errorf("序列化结果里应包含 %s:%s", key, data)
}
}
}