feat: !playnext command and /play-next-song endpoint

Mirror of !play / /play-song but uses queue.addNext to splice in at
currentIndex+1 instead of clearing the queue. Idle bot still starts
the song immediately. Adds 'playnext' / 'pn' to AUDIO_COMMANDS, the
command switch, and the help text.

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 16:37:52 +08:00
1 parent 11c6a3f51b
commit 3e795d9a83
2 files changed
+65

No files matched your search

+31
View File
@@ -254,6 +254,8 @@ export class BotInstance extends EventEmitter {
const AUDIO_COMMANDS = new Set([ const AUDIO_COMMANDS = new Set([
"play", "play",
"add", "add",
"playnext",
"pn",
"next", "next",
"skip", "skip",
"prev", "prev",
@@ -270,6 +272,9 @@ export class BotInstance extends EventEmitter {
return this.cmdPlay(cmd); return this.cmdPlay(cmd);
case "add": case "add":
return this.cmdAdd(cmd); return this.cmdAdd(cmd);
case "playnext":
case "pn":
return this.cmdPlayNext(cmd);
case "pause": case "pause":
return this.cmdPause(); return this.cmdPause();
case "resume": case "resume":
@@ -428,6 +433,31 @@ export class BotInstance extends EventEmitter {
return `Added to queue: ${song.name} - ${song.artist} (position ${this.queue.size()})`; return `Added to queue: ${song.name} - ${song.artist} (position ${this.queue.size()})`;
} }
private async cmdPlayNext(cmd: ParsedCommand): Promise<string> {
if (!cmd.args) return "Usage: !playnext <song name>";
const provider = this.getProvider(cmd.flags);
const result = await provider.search(cmd.args, 1);
if (result.songs.length === 0)
return `No results found for: ${cmd.args}`;
const song = result.songs[0];
const wasIdle = this.player.getState() === "idle";
this.queue.addNext({ ...song, platform: provider.platform });
if (wasIdle) {
// Nothing playing — addNext fell through to push, promote and start.
this.queue.playAt(this.queue.size() - 1);
this.player.resetFailures();
const ok = await this.resolveAndPlay(this.queue.current()!);
this.emit("stateChange");
if (!ok) return `Cannot play: ${song.name}`;
return `Now playing: ${song.name} - ${song.artist}`;
}
this.emit("stateChange");
return `Up next: ${song.name} - ${song.artist}`;
}
private cmdPause(): string { private cmdPause(): string {
this.player.pause(); this.player.pause();
this.emit("stateChange"); this.emit("stateChange");
@@ -715,6 +745,7 @@ export class BotInstance extends EventEmitter {
`${p}play -b <song> — Search from BiliBili`, `${p}play -b <song> — Search from BiliBili`,
`${p}play -y <song> — Search from YouTube (yt-dlp)`, `${p}play -y <song> — Search from YouTube (yt-dlp)`,
`${p}add <song> — Add to queue`, `${p}add <song> — Add to queue`,
`${p}playnext <song> — Insert as next song (alias: ${p}pn)`,
`${p}pause/resume — Pause/resume`, `${p}pause/resume — Pause/resume`,
`${p}next/prev — Next/previous`, `${p}next/prev — Next/previous`,
`${p}stop — Stop and clear queue`, `${p}stop — Stop and clear queue`,
+34
View File
@@ -340,6 +340,40 @@ export function createPlayerRouter(
} }
}); });
// Insert a single song to play right after the current one.
// If nothing is playing, behaves like /play-song (start immediately).
router.post("/:botId/play-next-song", async (req, res) => {
try {
const bot = (req as any).bot;
const { song } = req.body;
if (!song || !song.id || !song.platform) {
res.status(400).json({ error: "song object with id and platform is required" });
return;
}
const queue = bot.getQueueManager();
const wasIdle = bot.getPlayer().getState() === "idle";
queue.addNext(song);
if (wasIdle) {
// No current playback — promote the just-added song to current
// and start it. addNext fell through to push, so it's the last item.
queue.playAt(queue.size() - 1);
bot.getPlayer().resetFailures();
const ok = await bot.resolveAndPlay(queue.current()!);
if (!ok) {
res.json({ ok: false, message: `无法播放「${song.name || song.id}」(区域/版权限制)` });
return;
}
res.json({ ok: true, message: `正在播放:${song.name || 'Unknown'} - ${song.artist || 'Unknown'}` });
return;
}
res.json({ ok: true, message: `已加入下一首:${song.name || 'Unknown'} - ${song.artist || 'Unknown'}` });
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
router.post("/:botId/add-song", async (req, res) => { router.post("/:botId/add-song", async (req, res) => {
try { try {
const bot = (req as any).bot; const bot = (req as any).bot;