diff --git a/src/bot/instance.ts b/src/bot/instance.ts index 23261ef..a44bfb2 100644 --- a/src/bot/instance.ts +++ b/src/bot/instance.ts @@ -553,4 +553,8 @@ export class BotInstance extends EventEmitter { isConnected(): boolean { return this.connected; } + + getIdentityExport(): string | undefined { + return this.tsClient.getIdentityExport(); + } } diff --git a/src/bot/manager.ts b/src/bot/manager.ts index 4dab98a..9043f93 100644 --- a/src/bot/manager.ts +++ b/src/bot/manager.ts @@ -131,12 +131,25 @@ export class BotManager { const bot = this.bots.get(id); if (!bot) throw new Error(`Bot ${id} not found`); await bot.connect(); + + // Mark as autoStart so it reconnects on Docker restart, and persist identity + const saved = this.database.getBotInstances().find((i) => i.id === id); + if (saved) { + this.database.saveBotInstance({ ...saved, autoStart: true }); + this.persistBotIdentity(saved, bot); + } } stopBot(id: string): void { const bot = this.bots.get(id); if (!bot) throw new Error(`Bot ${id} not found`); bot.disconnect(); + + // Mark as not autoStart so it stays stopped on Docker restart + const saved = this.database.getBotInstances().find((i) => i.id === id); + if (saved) { + this.database.saveBotInstance({ ...saved, autoStart: false }); + } } async loadSavedBots(): Promise { @@ -150,6 +163,7 @@ export class BotManager { port: saved.serverPort, queryPort: 10011, nickname: saved.nickname, + identity: saved.identity || undefined, defaultChannel: saved.defaultChannel || undefined, channelPassword: saved.channelPassword || undefined, }, @@ -163,18 +177,30 @@ export class BotManager { this.bots.set(saved.id, bot); - // Auto-connect in background (non-blocking, won't affect other bots) - bot.connect().then(() => { + // Only auto-connect bots that have autoStart enabled + if (saved.autoStart) { + bot.connect().then(() => { + // Persist identity after successful connection for future restarts + this.persistBotIdentity(saved, bot); + this.logger.info( + { botId: saved.id, name: saved.name }, + "Auto-connected saved bot" + ); + }).catch((err) => { + this.logger.error( + { err, botId: saved.id, name: saved.name }, + "Failed to auto-connect bot (start manually from Settings)" + ); + }); + + // Stagger connections to avoid overwhelming the TS server + await new Promise((resolve) => setTimeout(resolve, 1000)); + } else { this.logger.info( { botId: saved.id, name: saved.name }, - "Auto-connected saved bot" + "Loaded bot (autoStart disabled, not connecting)" ); - }).catch((err) => { - this.logger.error( - { err, botId: saved.id, name: saved.name }, - "Failed to auto-connect bot (start manually from Settings)" - ); - }); + } } this.logger.info( @@ -183,6 +209,13 @@ export class BotManager { ); } + private persistBotIdentity(saved: import("../data/database.js").BotInstance, bot: BotInstance): void { + const identity = bot.getIdentityExport(); + if (identity && identity !== saved.identity) { + this.database.saveBotInstance({ ...saved, identity }); + } + } + shutdown(): void { for (const bot of this.bots.values()) { bot.disconnect(); diff --git a/src/data/database.ts b/src/data/database.ts index 8f043ba..3ece408 100644 --- a/src/data/database.ts +++ b/src/data/database.ts @@ -24,6 +24,7 @@ export interface BotInstance { defaultChannel: string; channelPassword: string; autoStart: boolean; + identity?: string; } export interface BotDatabase { @@ -36,6 +37,15 @@ export interface BotDatabase { close(): void; } +function migrateSchema(db: Database.Database): void { + // Add identity column if it doesn't exist (migration for existing databases) + const columns = db.prepare("PRAGMA table_info(bot_instances)").all() as Array<{ name: string }>; + const hasIdentity = columns.some((c) => c.name === "identity"); + if (!hasIdentity) { + db.exec("ALTER TABLE bot_instances ADD COLUMN identity TEXT"); + } +} + function initTables(db: Database.Database): void { db.exec(` CREATE TABLE IF NOT EXISTS play_history ( @@ -58,7 +68,8 @@ function initTables(db: Database.Database): void { nickname TEXT NOT NULL, defaultChannel TEXT NOT NULL, channelPassword TEXT NOT NULL, - autoStart INTEGER NOT NULL DEFAULT 0 + autoStart INTEGER NOT NULL DEFAULT 0, + identity TEXT ); `); } @@ -67,6 +78,7 @@ export function createDatabase(dbPath: string): BotDatabase { const db = new Database(dbPath); db.pragma("journal_mode = WAL"); initTables(db); + migrateSchema(db); const insertHistory = db.prepare(` INSERT INTO play_history (botId, songId, songName, artist, album, platform, coverUrl) @@ -78,8 +90,8 @@ export function createDatabase(dbPath: string): BotDatabase { `); const upsertInstance = db.prepare(` - INSERT INTO bot_instances (id, name, serverAddress, serverPort, nickname, defaultChannel, channelPassword, autoStart) - VALUES (@id, @name, @serverAddress, @serverPort, @nickname, @defaultChannel, @channelPassword, @autoStart) + INSERT INTO bot_instances (id, name, serverAddress, serverPort, nickname, defaultChannel, channelPassword, autoStart, identity) + VALUES (@id, @name, @serverAddress, @serverPort, @nickname, @defaultChannel, @channelPassword, @autoStart, @identity) ON CONFLICT(id) DO UPDATE SET name = excluded.name, serverAddress = excluded.serverAddress, @@ -87,7 +99,8 @@ export function createDatabase(dbPath: string): BotDatabase { nickname = excluded.nickname, defaultChannel = excluded.defaultChannel, channelPassword = excluded.channelPassword, - autoStart = excluded.autoStart + autoStart = excluded.autoStart, + identity = excluded.identity `); const selectInstances = db.prepare(`SELECT * FROM bot_instances`); @@ -109,14 +122,19 @@ export function createDatabase(dbPath: string): BotDatabase { upsertInstance.run({ ...instance, autoStart: instance.autoStart ? 1 : 0, + identity: instance.identity ?? null, }); }, getBotInstances() { const rows = selectInstances.all() as Array< - Omit & { autoStart: number } + Omit & { autoStart: number; identity: string | null } >; - return rows.map((r) => ({ ...r, autoStart: r.autoStart === 1 })); + return rows.map((r) => ({ + ...r, + autoStart: r.autoStart === 1, + identity: r.identity ?? undefined, + })); }, deleteBotInstance(id) { diff --git a/src/ts-protocol/client.ts b/src/ts-protocol/client.ts index 63e5c04..a3bb428 100644 --- a/src/ts-protocol/client.ts +++ b/src/ts-protocol/client.ts @@ -53,6 +53,18 @@ export class TS3Client extends EventEmitter { } async connect(): Promise { + // Clean up any existing connection before creating a new one + if (this.client) { + this.logger.info("Cleaning up previous connection before reconnecting"); + try { + await this.client.disconnect(); + } catch { + // Ignore errors during cleanup + } + this.client = null; + this.clientId = 0; + } + const addr = `${this.options.host}:${this.options.port}`; this.logger.info({ addr }, "Connecting to TeamSpeak server (full client protocol)");