上一版的下拉里列的是「这次读到的实际档位」,没读到档位表(没开播)时下拉里就只剩 一项,想预先设「我要最高档」或「最低档」都做不到。改成按「实际档位里的第几档」来选: - 新增 `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 干净。
362 lines
12 KiB
Go
362 lines
12 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|