Major bug fixes and corner-case hardening across the backend, plus a
comprehensive feature test suite. All 94 unit tests + 51 integration
tests pass against a local TS3 server.
Lifecycle & state consistency
-----------------------------
- Bug A: startBot() now wraps connect() in a 15s deadline. A hung TS
handshake no longer blocks the /start HTTP call forever; the failing
instance is torn down and the caller gets a clean 500.
- Bug B: executeCommand rejects audio-dispatching commands (play, add,
next, skip, prev, playlist, album, fm) when the bot is disconnected.
Config-only commands (vol, mode, clear, stop, queue, now, lyrics)
still work so the UI stays usable while offline.
- Bug C: the tsClient 'disconnected' handler always clears player state
now, even when connect() never completed. A separate disconnectEmitted
flag guards duplicate external event emission. Previously an orphaned
connect attempt that idle-timed-out would leave playing=true forever.
- resolveAndPlay re-checks this.connected AFTER the URL-resolve await so
a stop() during the network call can't spawn ffmpeg on a disconnected
bot.
- connect() throws if disconnect() fired during the handshake await,
preventing a concurrent stop from being overwritten by a late connected
flag flip.
- startBot always disconnects the outgoing BotInstance before creating
a replacement, covering the mid-handshake case where isConnected()
still returned false but the library client was live.
- startBot now reuses the stored identity so server groups granted to
the bot survive restarts (was regenerating a fresh UID each time).
WebSocket reliability
---------------------
- BotManager extends EventEmitter and emits 'botInstance' whenever a
new instance is created. websocket.ts listens and re-attaches its
stateChange / connected / disconnected listeners immediately, fixing
the bug where player-bar UI never updated until manual refresh.
- attachedBots map now stores the BotInstance reference and detaches
stale listeners when the instance is replaced. Safety-net interval
(5s) also reconciles to catch anything missed.
- removeBot emits 'botInstanceRemoved' -> WS broadcasts a new
{type:"botRemoved", botId} message. Client drops the bot from its
local store instead of showing it as permanently offline.
HTTP input validation
---------------------
- /volume rejects non-number, NaN, Infinity, and out-of-range values
with a proper 400 instead of a 200 OK wrapping a usage-text string.
- /mode rejects anything not in {seq, loop, random, rloop} with 400.
- /seek rejects NaN / Infinity / negative (previously NaN slipped
through typeof==="number" and poisoned seekOffset).
- /play-at validates index < queue.size() BEFORE stopping current
playback (was silently killing the current song on invalid input).
- /play, /add, /playlist, /play-by-id, /add-by-id, /play-playlist
all honour platform=youtube now (previously fell through to netease
and silently played the wrong platform).
YouTube made truly optional
---------------------------
- Lazy checkYtDlpAvailable() runs `yt-dlp --version` once, caches only
positive results so users can install yt-dlp mid-run and have it
picked up without a restart.
- getAuthStatus() returns loggedIn=false with nickname
"YouTube (yt-dlp not installed)" when the binary is missing. UI can
grey out YouTube instead of silently returning empty searches.
- findYtDlp() picks .exe on win32 and bare binary elsewhere.
- /auth/status?platform=youtube now routes to the YouTube provider
instead of falling through to NetEase and leaking the NetEase
user's nickname + avatar.
- /auth/cookie rejects platform=youtube with 400 instead of clobbering
the NetEase cookie entry.
- README documents yt-dlp install paths (bin/ local vs PATH) and adds
a dedicated "Optional: YouTube source" section.
Bot Selector UI
---------------
- New power button in each row of the dropdown with play-state-aware
styling: disabled + wait-cursor during API call, green highlight when
connected, greys out when the bot is offline.
- Dropdown always visible when >=1 bot exists, bigger font + padding.
Queue correctness
-----------------
- PlayQueue.remove(current) now decrements currentIndex so next() in
sequential mode advances to the shifted song. Previously removing
the currently-playing track silently skipped the next track because
current() falsely reported it as active and next() then incremented
past it.
Vote-skip hardening
-------------------
- cmdVote: needed threshold is Math.max(1, ceil(users/2)) so a single
voter in an empty channel can't unanimously pass a vote with
needed=0.
- resolveAndPlay clears voteSkipUsers on every new track load so votes
can't leak across songs via cmdPlay/cmdPlaylist/cmdAlbum/cmdFm paths.
cmdAdd parity
-------------
- cmdAdd auto-plays the newly-added song if the player was idle,
matching /api/player/:id/add-by-id behaviour. Previously add'ing to
an empty queue on a connected+idle bot silently enqueued without
starting playback.
Test suite
----------
- scripts/test_full_feature.py — 51 tests across 10 groups exercising
every HTTP endpoint, WebSocket broadcasts, all music providers, bot
lifecycle, disconnected-state corners, seek validation, input
validation, and the main race conditions. Captures and restores the
target bot's initial state. Resilient to TS3 anti-flood via retry
with exponential backoff. Runs against a real local TS3 server.
- scripts/test_rapid_cycle.py — Bugs A/B/C regressions
- scripts/test_corner_cases.py — disconnect-during-connect race, config
commands while disconnected, etc.
- scripts/test_more_corners.py — resolveAndPlay race, seek NaN
- scripts/test_power_button.py — E2E for the new power button
- scripts/test_bot_remove.py — E2E for WS botRemoved broadcast
- scripts/test_playbar.py — player bar auto-show regression (updated
to restore bot state on exit)
- scripts/test_multibot.py — two-bot concurrent playback monitor
- src/audio/queue.test.ts — 4 new vitest cases for remove() edge cases
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement BiliBiliProvider for video-as-audio playback using direct
BiliBili API calls (search, video info, DASH audio URL extraction).
Add QR code login support and cookie persistence. Update FFmpeg to
send Referer header for BiliBili CDN URLs. Extend platform union type
to "netease" | "qq" | "bilibili" across all interfaces. Add -b flag
for chat commands and B站 badge in web UI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add isAdvancing guard to playNext() to prevent concurrent calls from
skipping songs (trackEnd + user !next race condition)
- Replace removeAllListeners in WebSocket setup with tracked named
listeners, attach only once per bot instead of every 5 seconds
- Add .catch() to unhandled playNext() in cmdVote
- Add sessionId counter to AudioPlayer to discard stale setTimeout
callbacks after stop()+play() transitions
- Defer nulling TS3 client until disconnect() promise resolves
- Add null checks for providers in play-by-id and add-by-id endpoints
- Add PCM buffer backpressure (pause FFmpeg at 960KB, resume at 480KB)
- Change volume slider from @input to @change to avoid flooding server
- Wrap error-reporting sendTextMessage in try/catch to prevent
unhandled rejection in error handler
- Clamp elapsed getter to song duration
- Log FFmpeg stderr at debug level instead of swallowing
- Return null from prev() in Sequential mode instead of wrapping
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Follows TS3AudioBot-NetEaseCloudmusic-plugin pattern:
- QueuedSong.url is now optional (metadata-only queue entries)
- resolveAndPlay() fetches URL on-demand right before playback
- Playlist/album/FM load instantly (only metadata stored)
- playNext() resolves URL lazily, skips up to 3 songs on failure
- Avoids expired URLs for songs deep in the queue
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>