669 Commits
Author SHA1 Message Date
Eric 2120d1a0d0 Merge pull request #1212 from Joftal/dev
feat(live/sooplive,recorders,configs,servers): Soop 取流降请求与密码出口封堵
2026-09-30 18:44:42 +08:00
Joftal 8683fcf37f fix(webapp): 明文配置页补一条快照过期提示
明文页展示的是打开那一刻的配置快照,而后台会在运行中续期各平台 Cookie。掩码字段
(如 Soop 密码)保存时始终沿用服务器上的最新原文,不受快照影响;Cookie 则是按
"这条与服务器当前值是否一致"判断用户有没有改过,拿着过期快照保存会把期间刚续好的
新值当成用户手写的旧改动顶回去。合并逻辑本身不改,只把风险与"刷新会覆盖未保存修改"
一并写在页面上。
2026-09-30 18:30:44 +08:00
Joftal a6286fd631 fix(live/sooplive): 自动登录冷却期内直接采用配置里已刷新的 Cookie
同账号多个房间共用 10 分钟自动登录冷却:房间 A 刚登录成功并把新 Cookie 写进配置后,
房间 B 这一路根本没发登录请求就被冷却挡住,只能继续举着自家的旧运行态干等下一个窗口,
19+ 私有房间整段录不上。

改为区分"被冷却挡住"与"登录确实失败"(新增哨兵错误 errAutoLoginCoolingDown),前者先
校验配置里那份 Cookie,可用就立即采用;与自家运行态相同则跳过,不重复敲校验接口,
校验结果本身也有 2 分钟缓存。Soop 账号密码是全局唯一一份,不存在跨账号串号,
因此不加 LoginID 守卫。配置里那份同样不可用时保持原冷却错误与运行态不变。
2026-09-30 18:30:35 +08:00
Joftal d40f034a97 fix(recorders): 延迟解析的四处静默失效与分段重启退避回退
- 本轮偏好改为在尝试开头取一次并固定下来:延迟解析要跑几百毫秒的平台请求,期间面板
  "切换清晰度"改的就是这个值,最终选择若重读全局配置会按两套偏好算,选中根本没解析过
  播放地址的那一路
- 选中的候选没有 Url 时带原因退出,不再在持有 currentFileLock 的情况下解引用空 URL:
  panic 被 goroutine 边界 recover 后写锁永不释放,面板查房间状态的读请求会全部挂住,
  表现成整个房间无响应而不是报错
- 全部候选解析失败不再回退全量解析:那等于同一次尝试里把每档的凭证与调度请求再敲一遍,
  而"全部失败"恰恰是最需要少发请求的状态;改为把第一档原因上抛,由 run() 的退避拉长间隔
- 解析途中房间才显示下播时,ErrLiveOffline 立即上抛交给 stopRetryForExplicitOffline,
  不再继续敲剩余档位,也不会让 recorder 卡在"录制准备中"反复重试
- TransferPipelineState 的 everHeldStream 改为单调或:新分段先被 Start,等转移执行时它
  自己可能已经录上过,无条件覆盖会把这份成功抹回旧实例的 false,后续重试退回 60s 长封顶

时序行为补常驻回归测试 recorder_stream_selection_test.go(7 项,含端到端 tryRecord 用例)。
2026-09-30 18:30:22 +08:00
Joftal a0c2469768 style(live/sooplive): 三条错误串改为动词开头,修 golangci-lint ST1005
upstream CI 全量跑 staticcheck,ST1005 不允许错误串以大写开头;这三条原本以 Soop 起头。
改成与本文件既有写法一致的动词开头(请求/获取/申请 Soop …失败),只调文案,不动逻辑与判断分支。
2026-09-30 08:47:51 +08:00
Joftal 4646611665 fix(configs,servers): Soop 密码不再从 JSON 接口下发并在明文页掩码
- SoopLiveAuth.Password 标记 json:"-":面板读取走专用状态接口与 raw-config YAML,
  /api/config 之类的 JSON 出口不再把明文密码带给浏览器或留在前端状态里。
- 设置明文页把密码位输出为占位符,提交时按"整节没提交/照抄占位符/只改用户名/显式换密码/
  留空清除"四种意图分别三方合并,既不会把占位符当成新密码存下,也不会用打开页面那一刻的
  旧快照倒回后台刚保存的凭证。
2026-09-29 20:00:19 +08:00
Joftal cdad77fa51 perf(live/sooplive,live,live/system,recorders): Soop 只解析选中清晰度并在同轮复用播放页
- 取流改为按 DeferredStreamResolver 延迟解析:先列档位、只给要录的那一路申请 aid,
  开播房间的请求量由 8 次降到 4 次;包装层与初始化窗口一并转发,平台未实现时返回
  ErrNotImplemented 由录制器回退全量解析,其他平台选路结果与旧算法逐位等价。
- 播放页与频道信息在 15s 窗口内复用成功结果:只缓存"已成功且开播"和 result 正常的
  播放信息,返回副本供调用方就地排序,登录态变化与房间配置更新时整体作废。
- 取流请求按原始 URL 做 singleflight 合并,同一轮的重复解析只发一次网络请求。
- 录制失败重试改为升档退避(5s 起倍增,从未录上过封顶 60s、本场录上过封顶 15s),
  间隔随分段重启继承,避免下播后仍以 5s 频率反复冲击平台接口。
2026-09-29 19:59:46 +08:00
Eric cb6837ce0a Merge pull request #1211 from Joftal/dev
fix(recorders/danmaku,configs,webapp): 弹幕去排队并修复四项静默失效
2026-09-29 09:57:43 +08:00
Joftal 6822de38c7 fix(recorders/danmaku,configs,webapp): 修描边虚化失效、刷屏叠印同车道与 128 迁移复活
- writeHeader:OutlineColour 跟随 opacity(BorderStyle=1 下只虚化填充会留实心黑边),outline 对 SC/Guard 全样式生效
- assignLane:并列最早车道从 nextLane 起扫描,占用点取 max 不被短弹幕提前抹掉;时间戳仍恒等于到达时刻,不级联
- configs:新增 danmaku_opacity_migrated 闸门,opacity 128→255 只在首次加载迁移,之后 128 按用户意图保留
- webapp 预览:描边 alpha 与文字 alpha 同口径,top/bottom 区域边界与后端车道一致
2026-09-29 09:50:20 +08:00
Joftal eb3dd548ed fix(recorders/danmaku,configs,webapp): 弹幕去排队并修复四项静默失效
滚动车道耗尽时不再推迟开始时间,改为占用一个净空窗口后复用,时间戳恒等于
到达时刻(#1178 的排队级联根因);scroll_time 用四舍五入的 ms/px 作为速度
唯一口径,事件时长与车道净空统一走 travelCS,消除"标称 5s 实跑 3.84s"。

opacity 原本全链路无效(Banner 样式不绘 BackColour),现按"文字透明度"实现
并把 128 视为未设置迁移到 255;outline 补齐到 SC/上舰样式;web 播放器底部
区域按像素中线取上整,不再整条车道凭空不显示。

yaml.Unmarshal 会顺着指针原地写入,值拷贝后仍指向包级默认配置,任意 YAML
都会永久污染全局默认,故新增 clonePointerFields 在加载/合并/取默认处换新指针。
2026-09-28 14:03:21 +08:00
Eric 230e24d17c Merge pull request #1210 from Joftal/dev
斗鱼自动续期cookie+部分问题修复
2026-09-26 00:42:43 +08:00
Joftal fc71b311ed fix(servers,configs,live/douyu): 明文配置并发提交与三处静默失效
- putRawConfig 改为锁内三方合并:继承与作废判定以提交时刻的最新配置为准,
  解析整份 YAML 那几十毫秒里后台续期写入的票据、登录 cookie 与 OpenList 令牌
  不再被入口快照倒回旧值(内存与磁盘同时倒带,重启也回不来)。
- 房间 URL 规范化后的去重改为逐字段补齐:整条丢弃会把重复条目上单独配的原画、
  输出目录、弹幕开关、转 mp4 无声清零,并随本次加载写回磁盘。
- 换票链未真正到达终点时不再报登录成功:3xx 无 Location、8 跳耗尽、终点 4xx/5xx
  一律判失败,否则残缺 cookie 落盘后面板显示已登录而实际录不上,下次续期还排到三天后。
- 扫码接口非 2xx 与缺少 error 字段的响应直接判失败,错误串不再夹带上游正文,
  避免一次性 token 与风控页原文进入 HTTP 响应和应用日志。

行为变更:明文页整块删除 cookies: 一节视为"未提交",不再清空登录态;
确要清除请写 cookies: {} 或把对应主机留空。
2026-09-26 00:23:21 +08:00
Joftal 73518c6723 Merge origin/master into dev
master 侧 1a629df 把配置改为不可变快照(GetCurrentConfig() 返回的对象不得就地
改写),与第十九轮 putRawConfig 的斗鱼凭证保护改到了同一段代码。

冲突解法:保留本分支一侧。CloneConfigShallow 会连 liveRoomIndexCache 一起换
新 map,因此"在副本上刷新索引"本身就不违反快照不可变这条不变量;同时把注释
口径修正为 concurrent map read and map write 会导致进程 fatal 退出。

验证:go run build.go dev / build-web 通过,go vet -tags dev ./src/... 干净,
go run build.go test 无失败(含 master 新增的 TestGetLiveRoomByUrlConcurrent
与本分支的 raw-config 凭证回归)。
2026-09-25 21:07:40 +08:00
Joftal 78c6336c93 feat(live/douyu,servers,configs,webapp): 斗鱼 cookie 后台自动续期(LTP0 + safeAuth 换票)
主站 acf_* 登录 cookie 实测仅约 6 天有效(Set-Cookie Max-Age=529200s),过期后取流降级、
弹幕登录失败且只能重新扫码。本功能用 passport 域的长期票据 LTP0 走 safeAuth 换票链自动续命。

- configs: 新增 douyu_auth(LTP0/DyDid/续期日程);Marshal 改 temp+fsync+rename 原子写并 Chmod 0600,
  rename 或建临时文件失败才回退原地写(Docker 单文件挂载场景);cookie 域名别名(douyu.com/m.douyu.com)
  入库统一归一到 www.douyu.com;写盘前从 cookie 串剥离 LTP0;凭证三处出口全部掩码
  (/api/config 走 json:"-"、raw-config 走 __BILILIVE_DOUYU_SECRET_HIDDEN__、debug 转储走 CloneForLog)
- live/douyu/refresh.go: 手动逐跳跟随 302 累积 Set-Cookie(8 跳上限、跳过空值),
  只放行 https 且 *.douyu.com 的跳点(douyu.com.evil.com、裸 http、片段藏域名一律拒绝),
  终点必须 200 且 follow/top3 探针返回 error=0 才算成功;非 200 归为临时错误、缺必需字段才判失效;
  字段级合并 + 设备号策略(dy_did/acf_did/acf_devid 复用旧值),合并结果按字段名有序写回避免无意义配置 diff
- 阈值调度:3d 到点续期、临时失败 6h 退避、判失效 24h、连败 6d 升档;cookie 确凿过期前不发取流请求
- servers/douyu_keeper.go: 30s 首检 + 每小时廉价检查,仅在存有 LTP0 时真正换票;
  续期结果只在 CAS 闭包内合并并校验 LTP0 归属(旧票据的迟到结果直接作废);
  失效只置 need_rescan 不清 LTP0,告警按 LTP0 去重且推送走独立 goroutine 不阻塞续期循环
- servers/handler.go: raw-config 区分"该节未提交"与"显式清空"、按 acf_uid 判换号、带锁提交并支持版本冲突重试;
  putLiveHostCookie 不误清长期票据;新增 GET /api/douyu/auth/status 供前端显示登录态与下次续期时间;
  B 站 pollBilibiliQRCode 保留上游原有字段(不再因结构体重序列化丢 refresh_token)并把 Set-Cookie
  登录字段拼进 data.cookies
- notify: 新增 SendSystemAlert,斗鱼需重新扫码时按已配渠道推送
- cmd/bililive: 启动 keeper 且不依赖 Web RPC 是否启用,保证无界面后台长期保活
- webapp: 斗鱼面板显示登录态/续期日程并做轮询会话隔离,房间列表标记登录态;
  B 站扫码成功后回填 cookie 并提示未落盘的后果
- 测试:新增 6 个文件覆盖换票链分类、失效与退避升档、掩码出口、原子写与回退、别名归一、设备号复用
  (全部 httptest/合成值,不触网、不含真实凭证)
- go.mod: google/uuid 由 indirect 归位为 direct(handler 直接引用)

实测记录:真机换票链有效态 302→200 JSONP+17 个 Set-Cookie、垃圾票据态 200 JSONP+零 cookie;
临时实例 30s 首检续期成功并热应用后,斗鱼原画以 60fps 连续录制跨过 5 分钟分段;
垃圾 LTP0 走 need_rescan + 5.99h 退避,原 cookie 与票据均未被破坏。
2026-09-25 20:45:43 +08:00
Joftal 43f3dfadb1 fix(live/system): InitializingLive 未把房间选项热应用到原始 Live
- BaseLive 是值嵌入,只调继承来的 UpdateLiveOptionsbyConfig 时,cookie/只录音频/清晰度只落在
  包装对象上;初始化完成后 OriginalLive 会顶掉本包装对象,期间热应用的 cookie 跟着一起丢了——
  表现为"重新扫码后老房间仍按扫码前的登录态取流",且没有任何提示
- 新增同名方法同时更新包装对象与 OriginalLive,错误优先返回包装层的
- 补 GetRoomID 委托:少了这层转发,WrappedLive 的类型断言只能走到本对象就返回空,
  斗鱼别名 URL(如 /lotterytimer)的弹幕录制会退回拿 URL 路径当房间号,弹幕登录直接失败
- 补用例:断言两个对象都收到选项、以及 OriginalLive 报错时能向上传播
2026-09-25 20:45:04 +08:00
Joftal f722eef366 fix(pkg/parser/ffmpeg): 请求头必须落在 -i 之前且只发一个 -headers
- 原实现把逐个 header 拼成多个 -headers 追加在 -c copy 之后,即 -i 之后;FFmpeg 中位于输入文件
  之后的选项属于输出文件,会被静默丢弃(实测服务端只收到 -user_agent/-referer,收不到 Cookie)
  ——带登录态取流的能力形同不存在
- 抽出 buildInputArgs/buildExtraHeaders:-user_agent、-referer、-headers 统一前置到 -i 之前,
  多个头合并为单个 -headers 并以 \r\n 分隔
- 合并时按 key 排序:map 遍历顺序随机,不排序会让每次录制生成不同命令行,无法复现与比对
- use_proxy 分支保持原样(命令行只有 -rw_timeout -i),限速条件一并收敛进 buildInputArgs
- 补 4 条用例覆盖头合并、头的相对位置、代理分支、限速开关
2026-09-25 20:44:52 +08:00
Joftal b615ca67b5 fix(pkg/hlsproxy): ReadTimeout 把 ffmpeg 复用的 keep-alive 连接掐断
- Go 的 http.Server.ReadTimeout 同时被当作 keep-alive 连接的空闲上限,空闲超时即由服务端关闭,
  客户端表现为 WSAECONNABORTED / ffmpeg 的 "Error number -10053 occurred"
- ffmpeg 的 HLS demuxer 正是复用同一条连接轮询播放列表,两次轮询之间可空闲几十秒,
  因此本地代理只能限"读请求头"这一段:改用 ReadHeaderTimeout,写超时留空(分段是流式透传)
- 上游资源超时仍由 p.client 负责,本地回环无需整体读超时
- 补回归用例:实测 Go 1.24 下只设 ReadHeaderTimeout 时空闲复用连接仍可用
2026-09-25 20:44:38 +08:00
Joftal 03059c7c0f feat(recorders/danmaku/douyin,configs): 抖音分享短链转长链与房间 URL 规范化 (issue #1185)
- 弹幕 client:v.douyin.com 分享口令运行时逐跳解析真实 roomId(域名白名单+ctx 30s 超时),修复手机版分享链接弹幕 WS 502
- 新增 sharelink.go:ResolveShareLongURL 锚定 owner.shortId 提取账号级 web_rid,添加房间时一次性转 live.douyin.com/<web_rid> 长链入库,规避短链固化场次跨开播失效;转换失败保留原短链不阻断添加
- NormalizeLiveRoomUrl:live.douyin.com/?live_web_rid=<n> 首页 query 形态规范化为长链(path 空才转换,只认 live_web_rid 不认 room_id),存量坏房间加载期自动修复
- newConfigPostProcess:规范化后同 URL 房间去重合并(is_listening 取 OR、notify_only 取 AND),堵升级后"query/别名存量条目+长链"双条目双录
2026-09-23 15:02:25 +08:00
Joftal 7fc3658a6b feat(live/douyu,webapp): 斗鱼扫码登录与 cookie 登录态录制
签名/取流/下载全链路携带 cookie,消除匿名拉流约 5 分钟断流;
新增扫码登录面板(自动落盘热应用、到期倒计时);m.douyu.com 入口规范化为 www
2026-09-23 10:45:36 +08:00
Eric ad8a9f2192 Merge branch 'bililive-go:master' into master 2026-09-22 15:06:03 +08:00
kira1928 ef71711a7c Merge pull request #1198 from bililive-go/fix/config-snapshot-concurrent-map
fix: 消除配置快照并发读写导致的 fatal 崩溃
2026-09-04 01:58:56 +09:00
kira1928 d32204dfcc Merge pull request #1175 from bililive-go/fix/e2e-active-recordings
fix(test): 修复 E2E active_recordings 泄漏并终止渲染失败的录制重试
2026-09-04 01:56:25 +09:00
1a629df637 fix: 消除配置快照并发读写导致的 fatal 崩溃
GetLiveRoomByUrl 在索引缓存未命中时会调用 RefreshLiveRoomIndexCache
写入共享的不可变配置快照的 map。该函数被列表接口、录制、监听等大量
goroutine 高频并发调用,一个 goroutine 写 map 而另一个读同一 map 时,
触发 Go 运行时的 fatal error(concurrent map read and map write),
整个进程直接退出——表现为「开始录制后一段时间报错崩溃」。

配置采用不可变快照架构(Update 复制-修改-原子替换,替换前统一
RefreshLiveRoomIndexCache),已存入的快照不应再被就地修改。本次修复:

- GetLiveRoomByUrl 改为纯只读:命中缓存直接返回,未命中回退到只读线性
  扫描,不再写共享快照的 map。
- 移除 putConfig / putRawConfig 中对共享快照的 RefreshLiveRoomIndexCache
  调用(Marshal 不需要该缓存,后续查找也已是只读)。
- 删除无调用者且同样就地修改快照的死方法 (*Config).RemoveLiveRoomByUrl。
- 新增并发回归测试 TestGetLiveRoomByUrlConcurrent。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-04 00:49:03 +09:00
kira1928 37d1d0e7d7 Merge pull request #1181 from bililive-go/fix/disable-platform-rate-limit
fix: 暂时关闭平台级访问限流
2026-08-28 23:31:52 +09:00
kira1928 45dcc52ba3 fix: 同步平台限流禁用状态 2026-08-23 04:09:33 +09:00
kira1928 8dc79754f8 Merge origin/master into fix/e2e-active-recordings 2026-08-23 04:05:36 +09:00
kira1928 7dcee5132b fix: 暂时关闭平台级访问限流 2026-08-23 01:46:57 +09:00
Eric ea8c24305c Merge pull request #1174 from Joftal/dev
fix(notify): 修复录制摘要通知在 Pipeline 处理前发送导致文件列表不准确
2026-08-18 10:25:10 +08:00
kira1928 ad81d2b9cf fix: 根据 review 收紧录制器停止与 E2E 清理 2026-08-18 01:10:20 +09:00
524f3329c7 fix(test): 修复 E2E active_recordings 泄漏并终止渲染失败的录制重试
E2E 中 update-full/update-workflow 的 "active_recordings 为 0" 断言偶发失败,根因有三:

1. fixture 模板 out_put_tmpl 使用了 live.Info 上不存在的字段 {{.StartTime}},
   导致每次文件名渲染都失败(自 #1061 起)。改为合法的时间戳写法。
2. recording.spec.ts 通过 UI 添加的 dev 直播间从不清理;workers=1 下所有 spec
   共用同一后端进程,残留房间被 mock 持续判定为在播,泄漏进后续 spec 的断言。
   增加 afterAll 统一删除全部直播间。
3. 文件名渲染失败后 recorder 会以 5s 间隔无限重试、刷屏且被计入活跃录制数。
   现改为派发一次 LiveEnd 交由 manager 回收(与 stopRetryForExplicitOffline
   同构),避免无意义的重试风暴。

并将两处 active_recordings 断言改为轮询,容忍录制器回收的短暂延迟。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 22:51:54 +09:00
JoftalandKscc 5cc21c8afd fix(pipeline): immediate 模式下后续阶段替换文件后上传标记丢失
immediate 模式下 cloud_upload 最先执行,上传 .flv 后设置 Metadata[uploaded]。
后续 fix_flv 阶段创建新 FileInfo 替换原始 .flv,新对象无 Metadata,
上传标记丢失。.ass 不经过 fix_flv,标记保留。

修复:
1. 上传循环中上传成功时立即设置 Metadata[uploaded](不依赖删除逻辑块)
2. PipelineContext 新增 UploadedPaths 记录已上传文件路径
3. 所有阶段执行完毕后,按基名匹配为被替换的文件恢复上传标记

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 17:44:42 +08:00
JoftalandKscc e52df5b4f9 fix(pipeline): immediate 模式下上传文件未标记 Metadata[uploaded]
Metadata[uploaded] 的设置在 if !uploadFailed && uploadTiming != 'immediate'
块内,immediate 模式下整个块被跳过,uploaded 文件从未被标记。
extractUploadedDetails 读取 LastStageFiles 时找不到 uploaded 标记,
通知回退到无上传标注的格式。

修复:在上传循环中上传成功时立即设置 Metadata[uploaded] = true,
不依赖后续删除逻辑块。同时修复 cleanupOrphanedAssFiles 未检查 .ass
是否已被 deleteMarkedFiles 删除的问题。

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 17:28:18 +08:00
JoftalandKscc 4fbbd52f32 fix(pipeline,recorders): cleanupOrphanedAssFiles 未检查 .ass 是否已被删除
deleteMarkedFiles 删除 .ass 后不加入 kept,但 cleanupOrphanedAssFiles 的
第二轮(处理 Pipeline 中的 .ass)只检查关联视频是否存在,不检查 .ass 本身。
当 .mp4 存在时,已删除的 .ass 被错误保留在 kept 中,CurrentFiles 多出一个
条目,导致 onPipelineTaskComplete 的 uploaded/kept 分类错误。

修复:第二轮先检查 os.Stat,.ass 已不存在则跳过(不加入 kept)。

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 17:23:49 +08:00
JoftalandKscc 543b04e67d fix(recorders): 部分上传时回调丢失已上传文件详情
onPipelineTaskComplete 仅在 CurrentFiles 为空(全部删除)时从 LastStageFiles
提取 uploaded 详情。当部分文件被上传删除、部分保留时(如 immediate 模式),
CurrentFiles 非空走 else 分支,已上传文件从 CurrentFiles 消失,LastStageFiles
的上传标记被忽略。

修复:始终从 LastStageFiles 提取 uploaded 详情,再合并 CurrentFiles 中未被
上传标记覆盖的保留文件。回退逻辑仅在两者都为空时触发。

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 17:18:34 +08:00
JoftalandKscc 77ba76cfa3 fix(pipeline): LastStageFiles 快照被 deleteMarkedFiles 清除上传标记
executor 保存 LastStageFiles 快照后调用 deleteMarkedFiles,后者清除已删除
文件的 Metadata[uploaded]。由于快照与 files 共享底层数组,LastStageFiles
中的上传标记也被清除。extractUploadedDetails 读取时无 uploaded 标记,
通知回退到 r.recordedFiles(无上传标注)。

修复:保存快照前深拷贝 files slice,断开与 deleteMarkedFiles 的共享。

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 16:56:22 +08:00
JoftalandKscc 8740b943e0 fix(recorders,webapp): 分段重启时摘要在 Pipeline 完成前发出及预览 burnDelSource 错误
1. 分段重启时序问题:CloseForRestart 先调用 r.Close() 让 run() 退出,
   run() 的 defer 触发 sendAccumulatedSummary,此时 suppressSummary
   可能还未设置(在 pipelineState 中需要锁),导致摘要在 Pipeline 完成前
   发出(pipelineDetails 为空,回退到 recordedFiles,无上传标记)。
   Pipeline 完成后回调发现 summarySent=true,跳过发送,上传详情丢失。

   修复:新增 closeForRestart 原子标记,在 CloseForRestart 中先设置标记
   再调用 Close()。run() 的 defer 检查此标记,跳过 sendAccumulatedSummary。
   TransferPipelineState 完成后由 onAllTasksDone 回调发送完整摘要。

2. 预览 burnDelSource=false 在 immediate 模式下错误标记 .mp4 为 uploaded。
   .mp4 在 immediate 模式下不会被上传(cloud_upload 最先执行),应保持 kept。

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 16:38:00 +08:00
JoftalandKscc b54af8ad62 fix(webapp): immediate 模式下预览上传列表和中间产物标记错误
immediate 模式下 cloud_upload 最先执行,上传原始 .flv 和 .ass,
后续处理阶段产出的 .mp4/.mkv 不会被上传。但预览逻辑把 .flv 标记为
intermediate(不上传),把 .mp4 标记为 uploaded,与实际完全相反。

修复:immediate 模式下,源文件用 uploaded_deleted(已上传后被处理阶段
替代删除),处理阶段产出的 .mp4/.mkv 保持 kept(未被上传)。
after_process 模式下 intermediate 逻辑不变。

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 16:22:14 +08:00
JoftalandKscc 42f8f9b519 fix(webapp): 预览未区分中间产物导致上传列表和保留列表错误
预览用 'deleted' 同时表示「被处理阶段替代的源文件」和「被删除的文件」,
导致:1) 中间产物(如转码后的 .mp4)误出现在上传列表,2) 最终视频误出现在
保留列表,3) 孤儿 .ass 清理逻辑误判视频存在。

修复:新增 'intermediate' 状态表示中间产物(被转码/烧录替代的源文件)。
- intermediate 不参与上传(仅最终产物上传)
- intermediate 在预览中显示为「删除」
- 孤儿 .ass 检查正确识别 intermediate 视频为不存在

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 16:09:13 +08:00
JoftalandKscc 54678eb83f fix(recorders): 通知合并时清理无关联视频的孤立 .ass 文件
DAA 发现的 .ass 标记为 Deletable(不标记 uploaded),executor 删除视频后
.ass 从 recordedFiles 加入 merged,但通知误显为本地保留。

修复:合并后检查 .ass 的关联视频是否在列表中,无视频的 .ass 被移除。
与 executor 的 cleanupOrphanedAssFiles 逻辑一致。

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 16:01:53 +08:00
JoftalandKscc f6002f088b fix(pipeline): 发现的 .ass 文件不应标记为已上传
deleteAllAfter 和 deleteAfter 路径中,从磁盘发现的关联 .ass 文件被标记
Metadata['uploaded'] = true,但实际未上传到云存储。通知错误将其报告为
「已上传到云端」。

修复:发现的 .ass 仅标记 Deletable(由 executor 删除),不标记 uploaded。
已通过 uploadSubtitles 上传的 .ass 不受影响(在上传循环中正确标记)。

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 15:59:24 +08:00
JoftalandKscc 15c973411f fix(webapp): 预览兜底逻辑未正确识别已删除视频导致 .ass 误显为保留
兜底逻辑用 status !== 'deleted' && status !== 'uploaded_deleted' 判断视频
是否存在,但 deleteAfter 标记的视频状态是 'uploaded_delete'(无 d),
导致兜底逻辑认为视频仍存在,.ass 不被清理。

修复:改为只认 'kept' 和 'uploaded'(本地保留)的视频为存在,
其他状态(deleted、uploaded_delete、uploaded_deleted)均视为不存在。

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 15:54:12 +08:00
JoftalandKscc 7ed283cf14 fix(pipeline): burn_delete_ass + upload_subtitles 时 .ass 未被上传
burn 阶段将 .ass 标记为 Deletable 后,cloud upload 的上传循环跳过它,
导致 .ass 未上传但通知显示「已上传到云端」。

修复:
- 上传循环放行 Deletable 的 .ass(当 uploadSubtitles 时)
- 上传后标记 Metadata[uploaded],确保 executor 正确删除

修复后行为:
- burn_delete_ass=true + upload_subtitles=true:.ass 先上传再删除
- burn_delete_ass=true + upload_subtitles=false:.ass 不上传,直接删除
- burn_delete_ass=false + upload_subtitles=true:.ass 正常上传

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 15:31:39 +08:00
JoftalandKscc 9eb367623a fix(notify): 上传后不删除时摘要也应显示已上传文件
原逻辑把「已上传」等同于「已删除」,只有所有文件都上传且删除时才走
「已上传到云端」分支。上传但不删除时,通知错误显示「本地文件已清理」。

修复:分离 uploaded 和 kept 两个维度,支持三种通知格式:
- 全部上传:显示「已上传到云端」
- 部分上传+部分保留:显示两段(上传列表+本地列表+磁盘空间)
- 全部本地保留:显示文件列表+磁盘空间

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 15:20:33 +08:00
JoftalandKscc cdd0544d79 fix(pipeline,servers): 手动上传和配置保存遗漏 upload_subtitles
1. EnqueueUploadTask 构建 pipeline config 时未传递 OptionUploadSubtitles,
   手动单文件/批量上传不会发现关联 .ass 文件
2. handler.go 解析 cloud_upload 配置时遗漏 upload_subtitles 字段,
   前端开关保存无效

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 15:18:08 +08:00
JoftalandKscc 0d5f2edd2f fix(servers): 上传弹幕字幕开关无法保存
handler.go 解析 cloud_upload 配置时遗漏 upload_subtitles 字段,
前端发送的开关值被后端忽略,导致保存无效。

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 15:09:27 +08:00
JoftalandKscc 79426525e9 feat(webapp): 云上传新增「上传弹幕字幕」开关及文件处理预览
- EffectiveConfig 接口新增 upload_subtitles 字段
- CloudUploadSettings 新增开关,位于删除开关之后、OpenList 认证之前
- analyzeConfig 预览逻辑:after_process 模式下 .ass 标记为 uploaded,
  immediate 模式不标记(.ass 尚未生成),兜底逻辑保持无视频时删除 .ass

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 14:50:52 +08:00
JoftalandKscc f5716ad3de feat(pipeline): 支持上传 .ass 弹幕字幕文件到云存储
新增 upload_subtitles 配置项,启用后 cloud upload stage 会在上传循环前
从磁盘发现与视频文件同名的 .ass 字幕文件,加入 input 参与上传。

改动:
- configs: CloudUpload 新增 UploadSubtitles 字段
- pipeline/config: 新增 OptionUploadSubtitles,ConvertLegacyConfig 传递
- extract_cover: CloudUploadStage 新增 uploadSubtitles 字段,
  Execute 中扫描 input 视频文件发现关联 .ass 并加入上传队列,
  文件类型过滤放行 .ass

限制:仅 after_process 模式生效(immediate 模式下 .ass 尚未生成)。
需要同时开启弹幕录制(danmaku_enable)才有 .ass 文件可上传。

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 14:42:27 +08:00
JoftalandKscc ad5fa50a59 fix(pipeline): 云上传模式下 .ass 文件未标记 uploaded 导致摘要误显
cloud upload 的 deleteAllAfter 和 deleteAfter 模式都只处理视频文件,
关联的 .ass 字幕文件不在 pipeline 中,不会被标记 Metadata[uploaded]。
摘要将其作为本地文件显示,但实际已被删除。

修复:
- deleteAllAfter:加入 output 时即设置 Metadata[uploaded] = true
- deleteAfter:为已上传视频文件查找关联 .ass,加入 output 并标记 uploaded

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 14:26:30 +08:00
JoftalandKscc 6fde121e55 fix(pipeline,recorders): 修复分段重启相关的五个并发问题
1. redirectedTo 数据竞争:改用 atomic.Pointer,遍历 Load()、写入 Store()
2. merged 底层数组写冲突:快照时深拷贝,断开与 ps.details 的共享
3. 摘要发送先于状态转移:transferWg 屏障确保 Pipeline 状态转入后再发送
4. 回调注册先于任务可见:扩展 m.mu.Lock 覆盖 CreateTask + 回调注册,
   阻塞 pollLoop 的 scheduleNextTasks
5. 异步通知 goroutine 未跟踪:notifyWg 跟踪 go sendAccumulatedSummary,
   关闭流程中 PipelineManager.Close 后等待通知完成

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-17 11:34:59 +08:00
JoftalandKscc 04ffe41c3c fix(pipeline): 修复分段重启时 Pipeline 状态转移的三个问题
1. 连续重启 A→B→C 回调链断裂:用 redirectedTo 链替代指针覆盖,
   resolveState() 沿链找到最终活跃状态,回调正确操作最终 recorder 的计数器

2. data race:suppressSummary 从 recorder 移入 pipelineSharedState 用 mu 保护;
   r.pipelineState 指针字段不再被覆盖,消除 Lock/Unlock 不配对的竞态

3. extractUploadedDetails 未排除封面文件:增加 FileTypeCover 过滤,
   与 FileInfoToDetails 保持一致

Co-Authored-By: Kscc <noreply@owtffssent.com>
2026-08-14 17:09:10 +08:00