fix(player): bigger retry budget + return value for /play-playlist

The previous fix for "播放全部 没有反应" hooked into playNext but had
two problems:

1. Used !!queue.current() to detect success, which is always true
   after queue.next() advanced — so the response message would lie
   about playback starting even when all retries failed.

2. The default 3-retry budget is sized for "next song couldn't
   resolve, skip it" cases. User-initiated playlist plays commonly
   hit long contiguous runs of QQ 104003 songs in collected playlists
   (entire ACG/古风 packs can be uniformly unstreamable in some
   regions), so 3-4 attempts wasn't enough.

Make playNext return whether a song actually started, and accept a
maxRetries parameter. /play-playlist now uses 20 retries — high
enough to clear typical unplayable runs, bounded enough that fully
unstreamable playlists still surface a clear "none were playable"
message rather than hanging.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.7 committed 2026-05-06 15:25:59 +08:00
1 parent 82655afa20
commit 4301da2e37
2 files changed
+18 -14

No files matched your search

+13 -9
View File
@@ -736,21 +736,23 @@ export class BotInstance extends EventEmitter {
/** /**
* Advance the queue and play the next song. If the resolved URL fails * Advance the queue and play the next song. If the resolved URL fails
* (e.g., copyright/region restrictions for QQ), skips up to 3 more * (e.g., copyright/region restrictions for QQ), skips up to `maxRetries`
* songs looking for a playable one. Public so REST endpoints that * more songs looking for a playable one. Public so REST endpoints that
* seed the queue can fall back to this retry-skip behavior. * seed the queue can fall back to this retry-skip behavior.
*
* Returns true if a song actually started playing, false otherwise.
*/ */
async playNext(): Promise<void> { async playNext(maxRetries = 3): Promise<boolean> {
if (this.isAdvancing || !this.connected) return; if (this.isAdvancing || !this.connected) return false;
this.isAdvancing = true; this.isAdvancing = true;
try { try {
this.voteSkipUsers.clear(); this.voteSkipUsers.clear();
const next = this.queue.next(); const next = this.queue.next();
let started = false;
if (next) { if (next) {
let started = await this.resolveAndPlay(next); started = await this.resolveAndPlay(next);
if (!started) { if (!started) {
// Skip to next if URL resolve fails (up to 3 retries) for (let i = 0; i < maxRetries && this.connected; i++) {
for (let i = 0; i < 3 && this.connected; i++) {
const retry = this.queue.next(); const retry = this.queue.next();
if (!retry) break; if (!retry) break;
if (await this.resolveAndPlay(retry)) { if (await this.resolveAndPlay(retry)) {
@@ -772,8 +774,9 @@ export class BotInstance extends EventEmitter {
await this.refillFm(); await this.refillFm();
const refillNext = this.queue.next(); const refillNext = this.queue.next();
if (refillNext) { if (refillNext) {
await this.resolveAndPlay(refillNext); started = await this.resolveAndPlay(refillNext);
} else { }
if (!started) {
this.player.stop(); this.player.stop();
this.profileManager.onSongChange(null).catch(() => {}); this.profileManager.onSongChange(null).catch(() => {});
} }
@@ -783,6 +786,7 @@ export class BotInstance extends EventEmitter {
} }
} }
this.emit("stateChange"); this.emit("stateChange");
return started;
} finally { } finally {
this.isAdvancing = false; this.isAdvancing = false;
} }
+5 -5
View File
@@ -266,13 +266,13 @@ export function createPlayerRouter(
} }
// If the first picked song can't resolve (e.g., QQ song with no // If the first picked song can't resolve (e.g., QQ song with no
// streaming entitlement → result 104003), fall back to the same // streaming entitlement → result 104003), fall back to playNext's
// retry-skip behavior playNext uses for trackEnd auto-advance. // retry-skip behavior. Use a higher retry budget than the default
// Otherwise the bot would sit silently on a dead song. // trackEnd auto-advance because user-initiated playlist plays
// commonly have long contiguous runs of unplayable songs.
let started = first ? await bot.resolveAndPlay(first) : false; let started = first ? await bot.resolveAndPlay(first) : false;
if (first && !started) { if (first && !started) {
await bot.playNext(); started = await bot.playNext(20);
started = !!queue.current();
} }
const playing = queue.current(); const playing = queue.current();