mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
feat(#119): save/load queues + live-queue persistence + playKeepsQueue (backend)
Add three default-off capabilities that stop the play queue from being lost, all gated behind admin/independent toggles so existing behavior is unchanged until an operator opts in: - Named save/load of queues (Feature 1): new saved_queues table (per-user + reserved __shared__ owner, capped at 50 queues / 1000 songs, JSON song blob that degrades to empty on corruption); /api/saved-queues router (list/save/ load/delete with ownership 404s, inert 403 when disabled); chat commands !save / !load [-a] / !queues; BotInstance.loadSavedQueue (replace/append). - Auto-restore live queue across restart (Feature 2): PlayQueue.snapshot/restore, queue_state table (one row per bot), a debounced snapshot writer driven off stateChange, and restore+resume on connect. Cancels the pending snapshot on disconnect so a stale write can't wipe the row a restart must restore. - playKeepsQueue (Feature 3): BotInstance.playSingleSong funnels chat !play and the web /play-song route through one place; when enabled a single-song play inserts-after-current and jumps instead of clearing the queue. Config gains savedQueuesEnabled + playKeepsQueue (both default false, strict- coerced on load like spotify.enabled); the settings API round-trips them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b8ec50c7f7
commit
69a2e8c264
14 files changed
+1250
-26
No files matched your search
+15
-18
@@ -461,7 +461,11 @@ export function createPlayerRouter(
|
||||
}
|
||||
});
|
||||
|
||||
// Play a single song by ID — resolves URL on demand
|
||||
// Play a single song by ID — resolves URL on demand. Funnels through
|
||||
// bot.playSingleSong so the config.playKeepsQueue decision (clear-and-play vs
|
||||
// insert-and-jump, keeping the queue) lives in one place shared with chat
|
||||
// !play. Serialized via runExclusive like /play-now-song so concurrent
|
||||
// requests can't interleave the queue mutation + playback (#119).
|
||||
router.post("/:botId/play-song", authorize({ capability: "player.control" }), async (req, res) => {
|
||||
try {
|
||||
const bot = (req as any).bot;
|
||||
@@ -474,23 +478,16 @@ export function createPlayerRouter(
|
||||
rejectDisabledLocalAudio(res);
|
||||
return;
|
||||
}
|
||||
const queue = bot.getQueueManager();
|
||||
bot.getPlayer().stop();
|
||||
queue.clear();
|
||||
queue.add({ ...song, requestedBy: requesterName(req) });
|
||||
queue.play();
|
||||
|
||||
bot.getPlayer().resetFailures();
|
||||
const ok = await bot.resolveAndPlay(queue.current()!);
|
||||
// Sweep AFTER the new song is queued+resolved, so replaying a local song
|
||||
// that was still in the queue doesn't delete the file we're about to play.
|
||||
bot.cleanupQueuedLocalSongs?.("queue_replaced");
|
||||
if (!ok) {
|
||||
res.json({ ok: false, message: `无法播放「${song.name || song.id}」(区域/版权限制)` });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ ok: true, message: `正在播放:${song.name || 'Unknown'} - ${song.artist || 'Unknown'}` });
|
||||
// The cleanupQueuedLocalSongs sweep now lives inside playSingleSong's
|
||||
// clear branch — do NOT also call it here, or it would delete retained
|
||||
// local uploads in keep-queue mode.
|
||||
const body = await bot.runExclusive(async () => {
|
||||
const ok = await bot.playSingleSong({ ...song }, requesterName(req));
|
||||
return ok
|
||||
? { ok: true, message: `正在播放:${song.name || 'Unknown'} - ${song.artist || 'Unknown'}` }
|
||||
: { ok: false, message: `无法播放「${song.name || song.id}」(区域/版权限制)` };
|
||||
});
|
||||
res.json(body);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: (err as Error).message });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user