When the bot was moved to another channel, the channel it left kept the
now-playing description forever: updateChannelDescription always targeted
getChannelId(), which by then already reported the new channel.
Remember which channel we last wrote to. On a self clientMoved event,
clear that channel and, if a song is playing, write the description to
the new one. Stop now clears the channel we actually wrote to, so a
missed move event can't leave a stale description behind either.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
BotInstance loads the persisted custom avatar in its constructor and handed
it to profileManager.setCustomAvatar(). On an idle bot that method
immediately starts the three-step file transfer
(fileTransferInitUpload -> uploadFileData -> clientupdate) — but the
constructor runs long before tsClient.connect(), so TS3Client.client is
still null and the very first step throws "Not connected".
Scope of the bug: setCustomAvatar stores the buffer before attempting the
upload, and profileManager.onConnect() re-applies this.customAvatar once the
handshake completes, so the avatar itself did end up on the server. What the
premature call actually cost was a guaranteed-to-fail file transfer plus a
"Profile update failed" warning on every bot start — and every restart, since
manager.startBot() tears the instance down and reconstructs it. ("Not
connected" is not in handleFeatureError's unrecoverable list, so it never
disabled the avatar feature.)
Add loadCustomAvatar(), which only stores the buffer, and use it at the
constructor call site. onConnect() was already doing the real work, so
nothing is lost. Guard on length > 0 as well: avatarStore.write() is
delete-then-write, so a crash mid-write leaves a 0-byte file, and a 0-byte
Buffer is truthy — previously that took setCustomAvatar's else branch and
fired two more doomed calls (fileTransferDeleteFile + a clear).
setCustomAvatar keeps its immediate-apply behaviour, so editing the avatar
from the WebUI on a live bot still takes effect right away.
Reported-by: @shenmu-rua
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review feedback on PR #56:
1. setCustomAvatar(buf) now triggers applyIdleAvatar when the bot is idle
(currentSong=null OR avatarEnabled=false). Spec said this; original impl
only stored the buffer, so a fresh upload from Settings was invisible
until next stop event. Track currentSong in BotProfileManager for the
idle check.
2. onConnect drops the !avatarEnabled guard — on a fresh connect there's
no song playing yet, so the spec matrix wants the custom avatar shown
regardless of sync. Previously bots reconnected with a stale TS3
server-side avatar.
3. CustomAvatarRow: defer the initializing=false flip to nextTick so the
load-time data-url assignment's queued watcher sees initializing=true
and bails. Removes the redundant PUT-on-mount that echoed the just-
loaded bytes back to the server.
4. BotInstance avatar load wrapped in try/catch — a corrupt/locked file
no longer crashes startup; we log and continue with no custom avatar.
Tests rewritten: 10 cases covering the full behavior matrix
(idle vs playing × sync on/off × custom set/null × stop/connect).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Silent failure: logs showed "Client properties updated" / "Description
updated" / "Avatar updated" even though the bot's nickname never
changed and the avatar stayed as "loading image" on clients.
Root causes:
- TS6HttpQuery.clientUpdate ignored non-2xx responses, so 400 (bad
parameter) and 403 (insufficient permission) were reported as success.
- updateClientProperties built TS3-escaped strings (\\s for space) then
split them back into JSON props, so TS6 received literal backslashes
and rejected the nickname silently.
- handleFeatureError only matched textual "permission" errors; HTTP
4xx statuses weren't recognised and the feature retried every song.
- doAvatarUpload had no per-step logging, making it impossible to tell
whether a broken avatar came from init, the TCP 30033 transfer, or
the client_flag_avatar command.
Fixes:
- Add HttpQueryError (status/body/path); clientUpdate throws on non-2xx.
- Build a raw property map in updateClientProperties; escape only on
the TS3 wire path.
- Log HTTP status and updated prop names on success.
- handleFeatureError now treats HTTP 400/401/403 as unrecoverable.
- Debug-log each step of doAvatarUpload plus bytes/elapsedMs on success.
https://claude.ai/code/session_018NrpGWbQQTrahUVXyea5Jy
- Wrap clientedit (description) with 5s timeout to prevent blocking
channel description and now-playing updates if the command hangs
- Check generation counter in clearAvatar to avoid clearing a newer
song's avatar when stop→play happens in quick succession
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add generation counter to prevent stale avatar uploads from
overwriting newer song's profile when rapidly skipping tracks
- Fix nickname truncation to use UTF-8 byte length instead of JS
string length (TS3 counts bytes, Chinese chars are 3 bytes)
- Add timeout to clearAvatar file transfer (was missing)
- Extract withTimeout helper to deduplicate timeout logic
- Add BiliBili CDN thumbnail resize support (@200w_200h)
- Bump generation on reconnect to discard in-flight updates from
the old connection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Description via clientupdate is rejected (error 1538) on TS3 full-client
protocol. Switch to clientedit on the bot's own clid, which is how
TS3AudioBot handles it. Requires b_client_modify_description permission.
Also add sendCommandNoWait to TS3Client for fire-and-forget commands
(clientupdate, channeledit) that don't return timely responses.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add BotProfileManager that updates the bot's TeamSpeak presence when
songs change: album cover as avatar, song info in nickname, away status
toggled on stop/play. Each feature is independently configurable via
REST API and persisted to the database. Permission-safe — features that
fail due to insufficient server permissions are silently disabled until
reconnect. Description falls back to nickname display on TS3 (only
supported via TS6 HTTP Query).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>