Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.7 KiB
Auto-pause on empty channel — design
Issue: #79 item 3 Date: 2026-05-30 Status: Approved (brainstorm), pending implementation plan
Problem
When everyone leaves the bot's voice channel, music keeps playing to an empty room. The maintainer wants an option to auto-pause when the channel is empty (no disconnect) and resume when someone returns.
Decisions (from brainstorm)
- Global toggle, reusing the already-declared but currently dead
config.autoPauseOnEmpty(src/data/config.ts, defaulttrue). No per-bot granularity (YAGNI). - Event-driven, near-instant reaction (not the 30s poll alone) — subscribe to TS client enter/leave/move events; keep the existing 30s idle poll as a fallback.
- Auto-resume only what we auto-paused — a user-paused track is never auto-resumed.
- Independent of the existing idle-disconnect (
idleTimeoutMinutes): both share the same emptiness signal but act independently (pause immediately; disconnect after N minutes).
Current state (verified)
client.tsgetClientsInChannel()returns all clients in the bot's channel including the bot; callers compute "others" aslength - 1. No persistent roster.- The library emits
clientEnter/clientLeave/clientMoved;client.tscurrently only logsclientEnterand does not re-emit leave/moved. - The idle poller in
instance.ts(_startIdlePoller, every 30s) already computesuserCount = getClientsInChannel().length - 1and, when<= 0, schedules an idle-disconnect afteridleTimeoutMinutes. player.pause()/player.resume()already pause/resume without disconnecting (ffmpeg stays alive, no voice sent). The player only knowsidle|playing|paused— there is no auto-vs-user-pause distinction today.BotConfig.autoPauseOnEmptyexists (default true) but is read nowhere.
Design
Occupancy signal (shared)
Extract the idle poller's count into one method on BotInstance:
checkChannelOccupancy() → queries getClientsInChannel(), computes userCount = length - 1,
and drives both the existing idle-disconnect timer (unchanged behavior) and the new
auto-pause logic below. It is called by:
- the existing 30s poll (fallback), and
- new TS event handlers.
Event subscription
client.ts: subscribe to and re-emit clientEnter, clientLeave, clientMoved up to
BotInstance. BotInstance.setupTsEvents() calls checkChannelOccupancy() on each (a re-query
is simplest, since clientLeave carries no channel id). This gives near-instant pause/resume;
the poll remains as a safety net.
Auto-pause logic (inside checkChannelOccupancy)
Add a private autoPaused = false flag to BotInstance.
- Empty (
userCount <= 0): ifconfig.autoPauseOnEmptyandplayer.getState() === "playing"→player.pause(),autoPaused = true, emitstateChange. (Idle-disconnect timer still scheduled as today.) - Re-populated (
userCount > 0): ifautoPausedandplayer.getState() === "paused"→player.resume(),autoPaused = false, emitstateChange. (Idle timer cancelled as today.)
autoPaused bookkeeping (so user pauses are respected)
Clear autoPaused = false in cmdPause, cmdResume, cmdStop, cmdPlay, and on
connect/disconnect (the disconnected handler calls player.stop() → idle). Net effect: only a
track we auto-paused gets auto-resumed; a user-paused track stays paused when someone returns.
Config wiring
GET /api/bot/settings: includeautoPauseOnEmptyin the payload (alongsideidleTimeoutMinutes).POST /api/bot/settings: accept + validate a booleanautoPauseOnEmpty,saveConfig, and propagate to live bots via a newBotInstance.updateAutoPause(enabled)(mirrorsupdateIdleTimeout). Since the instance readsthis.config.autoPauseOnEmptylive, propagation can be as simple as updating the stored config reference / a field the check reads.- Frontend
Settings.vue→ the 行为设置 section (alreadybot.manage-gated): add a toggle forautoPauseOnEmptynext to the idle-timeout control; load it in the settings fetch and send it on save.
Components / files
src/ts-protocol/client.ts— subscribe + re-emitclientEnter/clientLeave/clientMoved.src/bot/instance.ts—autoPausedfield;checkChannelOccupancy()(refactored from the idle poller, drives idle + auto-pause); event handlers; clearautoPausedin user commands + connect/disconnect;updateAutoPause(enabled).src/web/api/bot.ts—GET/POST /settingshandleautoPauseOnEmpty.web/src/views/Settings.vue(+ player store settings load/save) — the toggle.src/data/config.ts— field already exists (no change beyond confirming default).
Testing
- Decision unit test (TDD): extract the pause/resume decision into a testable method, e.g.
applyOccupancy(userCount)operating on an injected fake player (getState/pause/resume)- the
autoPausedflag + the config flag. Cases: empty+playing+enabled → pause +autoPaused; re-populated+autoPaused+paused → resume + clear; re-populated when NOTautoPaused(user pause) → no resume; flag disabled → no pause; empty while idle (not playing) → no-op.
- the
- API test:
GET/POST /api/bot/settingsround-tripsautoPauseOnEmpty(validates boolean, persists, propagates). - Live TS event wiring is verified by code review + a manual run (can't unit-test a real server).
Non-goals
- No per-bot toggle (global only). No change to idle-disconnect behavior. No new dependency.
- Reaction relies on events the bot can already see (same-channel members are always in view); no extra channel subscription needed.