diff --git a/src/ts-protocol/client.ts b/src/ts-protocol/client.ts index 761dd27..312eddf 100644 --- a/src/ts-protocol/client.ts +++ b/src/ts-protocol/client.ts @@ -56,11 +56,33 @@ export class TS3Client extends EventEmitter { const addr = `${this.options.host}:${this.options.port}`; this.logger.info({ addr }, "Connecting to TeamSpeak server (full client protocol)"); + // Throttle repeated "udp send error" warnings (fires every 20ms during playback if UDP breaks) + let udpErrorCount = 0; + let udpErrorTimer: ReturnType | null = null; + const throttledWarn = (msg: string, ...args: unknown[]) => { + if (typeof msg === "string" && msg.includes("udp send error")) { + udpErrorCount++; + if (udpErrorCount === 1) { + this.logger.warn(msg); + // After 2 seconds, log a summary and reset + udpErrorTimer = setTimeout(() => { + if (udpErrorCount > 1) { + this.logger.warn(`udp send error (repeated ${udpErrorCount} times, connection may be lost)`); + } + udpErrorCount = 0; + udpErrorTimer = null; + }, 2000); + } + return; + } + this.logger.warn(msg); + }; + this.client = new TS3FullClient(this.identity, addr, this.options.nickname, { logger: { debug: (msg) => this.logger.debug(msg), info: (msg) => this.logger.info(msg), - warn: (msg) => this.logger.warn(msg), + warn: throttledWarn, error: (msg) => this.logger.error(msg), }, }); diff --git a/src/web/websocket.ts b/src/web/websocket.ts index ec00d5e..916f820 100644 --- a/src/web/websocket.ts +++ b/src/web/websocket.ts @@ -65,7 +65,11 @@ export function setupWebSocket( }; const onDisconnected = () => { - broadcast({ type: "botDisconnected", botId: bot.id }); + broadcast({ + type: "botDisconnected", + botId: bot.id, + status: bot.getStatus(), + }); }; bot.on("stateChange", onStateChange); diff --git a/web/src/composables/useWebSocket.ts b/web/src/composables/useWebSocket.ts index b800363..b343cc2 100644 --- a/web/src/composables/useWebSocket.ts +++ b/web/src/composables/useWebSocket.ts @@ -40,7 +40,21 @@ export function useWebSocket() { store.updateBotStatus(data.botId, data.status); break; case 'botDisconnected': - store.removeBotStatus(data.botId); + // Bot disconnected from TS3 but still exists — update status, don't remove + if (data.status) { + store.updateBotStatus(data.botId, data.status); + } else { + const existing = store.bots.find((b) => b.id === data.botId); + if (existing) { + store.updateBotStatus(data.botId, { + ...existing, + connected: false, + playing: false, + paused: false, + currentSong: null, + }); + } + } break; } };