Skip to content

fix: 修复网易云听歌打卡上报时长偏差 - #288

Open
Silcat2011 wants to merge 1 commit into
SPlayer-Dev:devfrom
Silcat2011:fix/scrobble-report-duration
Open

Silcat2011 wants to merge 1 commit into
SPlayer-Dev:devfrom
Silcat2011:fix/scrobble-report-duration

Conversation

@Silcat2011

@Silcat2011 Silcat2011 commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

改动类型

  • 新功能(feat)
  • 缺陷修复(fix)
  • 重构 / 优化(不改变对外行为)
  • 文档(docs)
  • 其他(请在「改动说明」中注明)

是否包含破坏性变更

  • 是(请在「改动说明」中详细描述)
  • 否

改动说明

问题

使用网易云听歌打卡时,实际听到的时长与网易云侧记录的时长不一致,NCBL 与「原版日志」两种模式均受影响。实测:256s 的曲目完整听一遍只上报 128s。

原因

playProgress 把「达标判定时刻」当成了「上报时刻」:累计播放达到 min(时长/2, 240s) 时立即回调 onThreshold,neteaseScrobble 的 submit() 以该瞬间累计的 playedMs 写入 time。两种模式共用同一个 submit(),仅传输层不同。

修复

把「算听过一次」与「上报多少时长」拆成两个出口:

  • onThreshold:达标瞬间触发,语义不变 → lastfm/scrobbler.ts 无需改动(scrobble 是离散事件,timestamp 在加载时已固定,不需要最终时长)
  • onSettle(新增):轮次结算时触发,带本轮最终累计时长 → 网易云改在此刻提交

结算点补齐,避免达标后无结算而丢记录:

  • 切歌 / 播完沿用原有路径
  • 同曲重播在 rearm() 前先结算(上游不需要:它达标瞬间就发了)
  • 退出应用在 before-quit 补报,最多等待 2s;无待补发内容时不延迟退出

未达标的轮次 fired 为 false,不构成一次收听。

改动文件:playProgress.ts、neteaseScrobble.ts、core/index.ts、playProgress.test.ts。

关联 Issue

无

测试情况

playProgress.test.ts 9 个用例:两个出口的时序分离、结算取最终时长、切歌、同曲重播不丢本轮、退出补报、每轮只结算一次、shouldFire 中途开关可补发。

校验结果(Windows):

  • pnpm typecheck(node + web)通过
  • eslint 通过(无告警)
  • prettier --check 通过
  • tsx --test "electron/**/*.test.ts" "src/**/*.test.ts" 44/44 通过

截图 / 录屏

改动前(上报仅按歌曲一半时长记录:播放大约16分钟,实际上只上报大约8分钟):
https://github.com/user-attachments/assets/408904f5-7112-4a33-97bf-0d9abcd7dce4

改动后(报完整时长,且退出前补报完整累计时长):
https://github.com/user-attachments/assets/abb40ecb-4886-4698-b42d-a3f00ebadd40

附:应用崩溃后丢失进度,与网易云官方客户端端一致

自查清单

  • 本 PR 只包含一个主要功能 / 修复,没有夹带无关改动
  • 已在本地完整测试通过;AI 生成的代码同样自行测试并审阅过,未做未经验证的提交
  • 已运行 pnpm format,并确认 pnpm typecheck、pnpm lint 通过
  • 改动涉及原生模块时已 pnpm build:native 验证;未手写 native/*/index.d.ts(本 PR 不涉及原生模块)
  • 已向 dev 分支提交

@laoshuikaixue laoshuikaixue left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个定位可以确认:time 始终为 min(时长/2, 240s),当前问题在于结算触发会导致上报丢失

触发点只剩 load() 和 end(),两者之后都会执行 clear(),pending 会被清掉,因此过半后退出应用、进程崩溃、过半后暂停且不再恢复时,都不会发出上报;ipc/player.ts:806 的 before-quit 也没有 flush,而改动前在过半瞬间已经发出请求,上述场景下可以正常上报

从影响看,丢计数比时长偏小更值得优先避免,当前取舍与这一点不一致;新增的 5 个用例只覆盖阈值和结算时序,没有覆盖这些路径

shouldFire() 返回 false 的分支目前不会走到:注释写“保持达标状态,下次结算再判”,但 settle() 只有 load/end 两个调用点,二者后面都跟 clear(),pending 必然被清,不存在下一次结算

用例 4 断言的是 A 不发、B 自己发,A 的 pending 是否保留不影响结果,因此覆盖不到这一点;改动前 maybeFire 会在 tick/setPlaying 中反复判断 shouldFire,开关中途关闭再打开仍可补发,改动后只剩结算时的一次判定

lastfm 的时序也被改了,lastfm/scrobbler.ts:31 的 onThreshold 不消费 playedMs,timestamp 在 load 时确定,“过半即报”符合它需要的时机,推迟到曲末提交没有明显收益,只会增加上述丢失风险

修改思路是把“何时算听过一次”和“time 填多少”拆开:触发时机不变,轮次结束时由 neteaseScrobble 通过 elapsedMs() 补一次时长;如果确实要延后提交,结算点需要覆盖 before-quit、窗口关闭、暂停超时,并补一条“过半后退出”的用例

另外,网易云侧的偏差是如何观察到的?可以补一组本地实际收听时长与网易云记录的对照

@Silcat2011

Copy link
Copy Markdown
Contributor Author

感谢laoshuikaixue的建议喵!已修改,延后提交。至于为什么不在上报后再补一次时长,原因是这样的:
实测:达标发 time=128、播完再补 time=255 → 时长 +383s、播放次数 +2,两种模式是一样的。
服务端语义由此得:每个 play / PLD 事件都记一次「听过」,时长累加而非取最后一次。补发会让次数翻倍、时长变 1.5 倍;把这一对拆到不同时刻(达标发 PLV、结束发 PLD)同样无效——计数由 play / PLD 驱动。

@Silcat2011
Silcat2011 force-pushed the fix/scrobble-report-duration branch from f31ea80 to c08b6a5 Compare October 1, 2026 10:20

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants