Files
teamspeak-music-bot/src/data/database.ts
T
Claude 2559701e55 Fix Docker restart crash: respect autoStart flag and persist identity
Root cause: on Docker restart, loadSavedBots() unconditionally connected
ALL saved bots regardless of autoStart flag, causing a rapid
connect/disconnect loop. Additionally, bot identities were regenerated
on every restart, causing TS server conflicts with stale sessions.

Changes:
- loadSavedBots() now only auto-connects bots with autoStart=true
- startBot/stopBot persist autoStart state so restart behavior matches
  user intent
- Bot TS3 identity is persisted to database and reused across restarts
- Database schema migrated to include identity column
- TS3Client.connect() cleans up existing connection before reconnecting
- Stagger bot connections by 1s to avoid overwhelming the TS server

https://claude.ai/code/session_01L2kEV2M1QFWMCyPtLU5LgC
2026-04-07 12:15:39 +00:00

150 lines
4.1 KiB
TypeScript

import Database from "better-sqlite3";
export interface PlayHistoryEntry {
botId: string;
songId: string;
songName: string;
artist: string;
album: string;
platform: "netease" | "qq" | "bilibili";
coverUrl: string;
}
export interface PlayHistoryRecord extends PlayHistoryEntry {
id: number;
playedAt: string;
}
export interface BotInstance {
id: string;
name: string;
serverAddress: string;
serverPort: number;
nickname: string;
defaultChannel: string;
channelPassword: string;
autoStart: boolean;
identity?: string;
}
export interface BotDatabase {
db: Database.Database;
addPlayHistory(entry: PlayHistoryEntry): void;
getPlayHistory(botId: string, limit: number): PlayHistoryRecord[];
saveBotInstance(instance: BotInstance): void;
getBotInstances(): BotInstance[];
deleteBotInstance(id: string): boolean;
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 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
botId TEXT NOT NULL,
songId TEXT NOT NULL,
songName TEXT NOT NULL,
artist TEXT NOT NULL,
album TEXT NOT NULL,
platform TEXT NOT NULL,
coverUrl TEXT NOT NULL,
playedAt TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS bot_instances (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
serverAddress TEXT NOT NULL,
serverPort INTEGER NOT NULL,
nickname TEXT NOT NULL,
defaultChannel TEXT NOT NULL,
channelPassword TEXT NOT NULL,
autoStart INTEGER NOT NULL DEFAULT 0,
identity TEXT
);
`);
}
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)
VALUES (@botId, @songId, @songName, @artist, @album, @platform, @coverUrl)
`);
const selectHistory = db.prepare(`
SELECT * FROM play_history WHERE botId = ? ORDER BY id DESC LIMIT ?
`);
const upsertInstance = db.prepare(`
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,
serverPort = excluded.serverPort,
nickname = excluded.nickname,
defaultChannel = excluded.defaultChannel,
channelPassword = excluded.channelPassword,
autoStart = excluded.autoStart,
identity = excluded.identity
`);
const selectInstances = db.prepare(`SELECT * FROM bot_instances`);
const deleteInstance = db.prepare(`DELETE FROM bot_instances WHERE id = ?`);
return {
db,
addPlayHistory(record) {
insertHistory.run(record);
},
getPlayHistory(botId, limit) {
return selectHistory.all(botId, limit) as PlayHistoryRecord[];
},
saveBotInstance(instance) {
upsertInstance.run({
...instance,
autoStart: instance.autoStart ? 1 : 0,
identity: instance.identity ?? null,
});
},
getBotInstances() {
const rows = selectInstances.all() as Array<
Omit<BotInstance, "autoStart" | "identity"> & { autoStart: number; identity: string | null }
>;
return rows.map((r) => ({
...r,
autoStart: r.autoStart === 1,
identity: r.identity ?? undefined,
}));
},
deleteBotInstance(id) {
const result = deleteInstance.run(id);
return result.changes > 0;
},
close() {
db.close();
},
};
}