Skip to content

[Bug]: 单曲循环时听歌打卡只报第一遍(ended 后不重新 load,打卡载荷已被清空) #340

Closed

Description

@nattwood913-web

版本:SPlayer Next v1.1.0(Windows 11 x64,安装版),已登录网易云账号,开着「启用听歌打卡」,「NCM 听歌打卡方式」选「原版日志接口」。dev 分支(4c60335d09)同样的写法,见下。

现象:播放模式设成「单曲循环」,一首网易云的歌连着放了约 30 分钟(时长 3:56,约 7 遍),网易云手机客户端的听歌排行里这首歌只多了 1 次。

原因(读代码得到,并把两段原样切出来跑过):

  1. 单曲循环时,渲染层在 ended 之后只做 seek(0) 加 play(),不重新 load:dev 分支 src/core/player/events.ts 第 52–55 行 finishCurrentTrack。
  2. 主进程收到 ended 时调 neteaseScrobble.onEnded(),里面 progress.end() 清掉了计时载荷,并把 current 置为 null:dev 分支 electron/main/services/neteaseScrobble.ts 第 120–125 行。
  3. 之后没有 load,onTrackLoaded 不会再被调,载荷一直是空的:playProgress 的 maybeFire 在 payload == null 时直接返回;onPosition 里「跳回阈值之前就 rearm」那一段要求 current 不为空,也进不去。所以单曲循环从第二遍起一次都不报。

列表循环不受影响:每一遍都经过 nextTrack → loadTrack → player:load → onTrackLoaded。

复现:从 v1.1.0 安装包的 out/main/index.js 原样取出 createPlayProgress(第 2925–2992 行)和网易云打卡那一段(第 5702–5773 行),把 Date.now 换成假时钟、callNetease 换成计数,模拟一首 236 秒的歌每秒一个 position 事件、放完发 ended:

模式 放了几遍 上报几次 上报时刻(秒)
单曲循环(ended 后不重新 load,只重新 play) 8 1 118
列表循环(每遍都 load) 8 8 118, 355, 592, 829, 1066, 1303, 1540, 1777

同样受影响的:Last.fm 的 scrobble 走同一个 createPlayProgress,它的 onEnded 也把 current 置空(v1.1.0 out/main/index.js 第 3026–3030 行),单曲循环下应该也只报第一遍(这一条只读了代码,没跑)。

建议:单曲循环重新开始播放时,让主进程把这一轮当作重新加载。比如渲染层单曲循环那条路走一次轻量的「重新开始」IPC,主进程据此对当前曲目重新 onTrackLoaded;或者 onEnded 先留住上一首的载荷,紧接着同一曲目从 0 开始播放时 load 回去。

Activity

  1. github-actions commented on Oct 1, 2026

    @github-actions

    👋 @nattwood913-web

    感谢提交 Issue!我们已经收到您的反馈,会尽快确认您的问题。

    Thanks for opening this issue! We have received your report and will review it as soon as possible.

  2. nattwood913-web commented on Oct 1, 2026

    @nattwood913-web
    Author

    补一个可以直接用的修法,对 dev 4c60335d09:

    主进程收到 ended 时,如果当前是单曲循环,就不清空网易云打卡的曲目,只把本轮计时清零重来;别的情形照旧调 onEnded()。主进程的循环模式来自 player:syncPlayMode(渲染层 init 和改模式时都会同步过来),MCP 的 get_playback_status 读到的也是它。FM 模式下渲染层不走单曲重播,会切下一首,onTrackLoaded 照常重置。两边模式万一没同步上,最坏就是退回现在的行为。

    diff --git a/electron/main/ipc/player.ts b/electron/main/ipc/player.ts
    index 543b4e8..e1f96dc 100644
    --- a/electron/main/ipc/player.ts
    +++ b/electron/main/ipc/player.ts
    @@ -30,6 +30,7 @@ import {
       setTraySongName,
       setTrayPlayState,
       setTrayPlayMode,
    +  getTrayPlayMode,
       setTrayLikeState,
     } from "@main/services/tray";
     import { setTaskbarThumbnailCover } from "@main/services/thumbnail";
    @@ -167,7 +168,12 @@ const registerNativeEvents = (inst: InstanceType<AudioEngineModule["AudioPlayer"
             wsBroadcast({ type: "ended" });
             mediaService.setPlayState({ status: "Paused" });
             lastfm.onEnded();
    -        neteaseScrobble.onEnded();
    +        // 单曲循环由渲染层 seek(0) 重播、不重新 load,打卡要保留当前曲目(FM 模式下渲染层会切下一首,load 时照常重置)
    +        if (getTrayPlayMode().repeat === "one") {
    +          neteaseScrobble.onRepeatOneRestart();
    +        } else {
    +          neteaseScrobble.onEnded();
    +        }
             setTaskbarProgress(-1);
             break;
           }
    diff --git a/electron/main/services/neteaseScrobble.ts b/electron/main/services/neteaseScrobble.ts
    index b318b72..b07e442 100644
    --- a/electron/main/services/neteaseScrobble.ts
    +++ b/electron/main/services/neteaseScrobble.ts
    @@ -123,3 +123,18 @@ export const onEnded = (): void => {
       current = null;
       lastPositionMs = 0;
     };
    +
    +/**
    + * 单曲循环自动重播:渲染层只 seek(0) + play(),不会再走 player:load / onTrackLoaded,
    + * 所以保留当前曲目、本轮计时清零重来,而不是像 onEnded 那样清空(#340)
    + */
    +export const onRepeatOneRestart = (): void => {
    +  if (!current) {
    +    onEnded();
    +    return;
    +  }
    +  cycleId++;
    +  progress.tick();
    +  progress.rearm();
    +  lastPositionMs = 0;
    +};

    验证过的:

    1. 在 v1.1.0 的 out/main/index.js 上做了同样的两处改动,把 createPlayProgress、网易云打卡那一段和 case "ended" 原样切出来,假时钟模拟一首 236.2 秒的歌:

      情形 放了几遍 原版上报 改后上报
      单曲循环 8 1 8
      列表循环(每遍都 load) 8 8 8
      单曲循环,第一遍中途暂停 10 分钟 3 1 3
      单曲循环,睡眠定时在第 2 遍末尾停下,空闲 1 小时后再切歌 2 1 2
      渲染层单曲循环、主进程以为是列表循环(没同步上) 8 1 1
      渲染层列表循环、主进程以为是单曲循环(没同步上) 8 8 8

      每次上报的 time 都是 118 秒(阈值),total 236。

    2. 引擎的事件顺序是在 node 里直接加载 audio-engine.node、放一段 1.5 秒静音看的:放完先发 ended,紧跟着 stateChanged: stopped;对停下的播放器 seek(0) 会自己发 stateChanged: playing,后面的 play() 不再发事件;停下以后 pause() 还会发 stateChanged: paused。所以重播以后计时由 onState(true) 正常接上,睡眠定时停在曲末也不会把空闲时间算进去(上表第四行按这个顺序跑的)。

    3. 改过的 app.asar 换进 v1.1.0 安装版,能正常启动,日志没有报错。

    没做的:没在 dev 上跑 typecheck 和测试;Last.fm 那边是同一种写法,没改,照这个思路补的话,每一遍要重新取 timestamp、重置 nowPlayingSent。网易云服务端对同一首歌连着上报怎么计数,还在真机上看。需要 PR 的话说一声。

  3. nattwood913-web commented on Oct 1, 2026

    @nattwood913-web
    Author

    补上一条评论说还在看的真机计数:读数对得上,但这一次不算干净。

    上面那份补丁换进 v1.1.0 安装版以后,打卡方式用「原版日志接口」(legacy),单曲循环放同一首歌(约 270 秒)两遍整首,第三遍没到阈值就停了。play_history 记了两条(第二条是单曲循环自动重播开的)。这首歌在网易云听歌排行里的次数从 18 涨到 20:20 是手机上读的;18 是按公开排行接口 play/record 的相对分推的,这个账号上「分=次数×5」有三个点对得上。同一时段用列表循环放另一首一遍,次数加 1。

    不干净的地方:同一时段还有一首歌的次数涨了 1,这台电脑上的 SPlayer 从没放过它(play_history 0 条),手机上也没放,来路不明。所以「补丁后两遍都算了」和「两遍只算了一遍、另一次来自别处」这两种读法,这次的读数分不开。NCBL 没在补丁上测。

  4. github-actions commented on Oct 24, 2026

    @github-actions

    🎉 @nattwood913-web

    感谢您的反馈!该问题已修复,并将在当前开发版或下一个正式版中生效。
    若您仍能复现该问题,欢迎重新提交新的 Issue。

    Thanks for your report! This issue has been resolved and will be available in the current dev build or next stable release.
    If you can still reproduce this problem, please open a new issue.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    resolvedFixed, pending release

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions