mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
feat: add SQLite database module with play history and bot instances
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6324953394
commit
8727605036
2 files changed
+225
No files matched your search
@@ -0,0 +1,97 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import { createDatabase, type BotDatabase, type BotInstance } from "./database.js";
|
||||||
|
|
||||||
|
describe("database", () => {
|
||||||
|
let botDb: BotDatabase;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
botDb = createDatabase(":memory:");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
botDb.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates tables on init", () => {
|
||||||
|
const tables = botDb.db
|
||||||
|
.prepare(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
||||||
|
)
|
||||||
|
.all() as Array<{ name: string }>;
|
||||||
|
const names = tables.map((t) => t.name);
|
||||||
|
expect(names).toContain("play_history");
|
||||||
|
expect(names).toContain("bot_instances");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("records and retrieves play history", () => {
|
||||||
|
botDb.addPlayHistory({
|
||||||
|
botId: "bot1",
|
||||||
|
songId: "song1",
|
||||||
|
songName: "Test Song",
|
||||||
|
artist: "Test Artist",
|
||||||
|
album: "Test Album",
|
||||||
|
platform: "netease",
|
||||||
|
coverUrl: "https://example.com/cover.jpg",
|
||||||
|
playedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
botDb.addPlayHistory({
|
||||||
|
botId: "bot1",
|
||||||
|
songId: "song2",
|
||||||
|
songName: "Another Song",
|
||||||
|
artist: "Another Artist",
|
||||||
|
album: "Another Album",
|
||||||
|
platform: "qq",
|
||||||
|
coverUrl: "https://example.com/cover2.jpg",
|
||||||
|
playedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const history = botDb.getPlayHistory("bot1", 10);
|
||||||
|
expect(history).toHaveLength(2);
|
||||||
|
expect(history[0].songName).toBe("Another Song");
|
||||||
|
expect(history[1].songName).toBe("Test Song");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saves and loads bot instances", () => {
|
||||||
|
const instance: BotInstance = {
|
||||||
|
id: "bot1",
|
||||||
|
name: "Music Bot",
|
||||||
|
serverAddress: "localhost",
|
||||||
|
serverPort: 9987,
|
||||||
|
nickname: "MusicBot",
|
||||||
|
defaultChannel: "Music",
|
||||||
|
channelPassword: "",
|
||||||
|
autoStart: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
botDb.saveBotInstance(instance);
|
||||||
|
const instances = botDb.getBotInstances();
|
||||||
|
expect(instances).toHaveLength(1);
|
||||||
|
expect(instances[0]).toEqual(instance);
|
||||||
|
expect(instances[0].autoStart).toBe(true);
|
||||||
|
|
||||||
|
// Test upsert
|
||||||
|
botDb.saveBotInstance({ ...instance, nickname: "UpdatedBot", autoStart: false });
|
||||||
|
const updated = botDb.getBotInstances();
|
||||||
|
expect(updated).toHaveLength(1);
|
||||||
|
expect(updated[0].nickname).toBe("UpdatedBot");
|
||||||
|
expect(updated[0].autoStart).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletes bot instance", () => {
|
||||||
|
botDb.saveBotInstance({
|
||||||
|
id: "bot1",
|
||||||
|
name: "Music Bot",
|
||||||
|
serverAddress: "localhost",
|
||||||
|
serverPort: 9987,
|
||||||
|
nickname: "MusicBot",
|
||||||
|
defaultChannel: "Music",
|
||||||
|
channelPassword: "",
|
||||||
|
autoStart: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(botDb.deleteBotInstance("bot1")).toBe(true);
|
||||||
|
expect(botDb.getBotInstances()).toHaveLength(0);
|
||||||
|
expect(botDb.deleteBotInstance("nonexistent")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
|
||||||
|
export interface PlayHistoryRecord {
|
||||||
|
id?: number;
|
||||||
|
botId: string;
|
||||||
|
songId: string;
|
||||||
|
songName: string;
|
||||||
|
artist: string;
|
||||||
|
album: string;
|
||||||
|
platform: string;
|
||||||
|
coverUrl: string;
|
||||||
|
playedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BotInstance {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
serverAddress: string;
|
||||||
|
serverPort: number;
|
||||||
|
nickname: string;
|
||||||
|
defaultChannel: string;
|
||||||
|
channelPassword: string;
|
||||||
|
autoStart: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BotDatabase {
|
||||||
|
db: Database.Database;
|
||||||
|
addPlayHistory(record: Omit<PlayHistoryRecord, "id">): void;
|
||||||
|
getPlayHistory(botId: string, limit: number): PlayHistoryRecord[];
|
||||||
|
saveBotInstance(instance: BotInstance): void;
|
||||||
|
getBotInstances(): BotInstance[];
|
||||||
|
deleteBotInstance(id: string): boolean;
|
||||||
|
close(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDatabase(dbPath: string): BotDatabase {
|
||||||
|
const db = new Database(dbPath);
|
||||||
|
db.pragma("journal_mode = WAL");
|
||||||
|
initTables(db);
|
||||||
|
|
||||||
|
const insertHistory = db.prepare(`
|
||||||
|
INSERT INTO play_history (botId, songId, songName, artist, album, platform, coverUrl, playedAt)
|
||||||
|
VALUES (@botId, @songId, @songName, @artist, @album, @platform, @coverUrl, @playedAt)
|
||||||
|
`);
|
||||||
|
|
||||||
|
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)
|
||||||
|
VALUES (@id, @name, @serverAddress, @serverPort, @nickname, @defaultChannel, @channelPassword, @autoStart)
|
||||||
|
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
|
||||||
|
`);
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getBotInstances() {
|
||||||
|
const rows = selectInstances.all() as Array<
|
||||||
|
Omit<BotInstance, "autoStart"> & { autoStart: number }
|
||||||
|
>;
|
||||||
|
return rows.map((r) => ({ ...r, autoStart: r.autoStart === 1 }));
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteBotInstance(id) {
|
||||||
|
const result = deleteInstance.run(id);
|
||||||
|
return result.changes > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
db.close();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in new issue
Block a user