feat: implement server password login, YouTube source, and improved bot selector UI

- **TS server password** (#7/#9): add serverPassword field across database,
  manager, bot API, and Settings UI — allows joining password-protected servers
- **YouTube audio source** (#1/#10): new YouTubeProvider using yt-dlp binary;
  adds -y flag in chat commands, /api/music supports platform=youtube,
  YouTube badge in SongCard, yt-dlp-wrap npm dependency
- **Bot selector UI** (#14): selector always visible (not just when >1 bot),
  bigger button with border and play-state indicator; per-bot URL routing at
  /bot/:id with BotRedirect view; copy-link button in dropdown

Closes #1, #7, #9, #10, #14

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Sonnet 4.6 committed 2026-04-09 11:58:07 +08:00
1 parent b20ddf2603
commit d5d6abeb1e
16 files changed
+391 -40

No files matched your search

+2
View File
@@ -62,6 +62,7 @@ describe("database", () => {
autoStart: true,
serverProtocol: "",
ts6ApiKey: "",
serverPassword: "",
};
botDb.saveBotInstance(instance);
@@ -90,6 +91,7 @@ describe("database", () => {
autoStart: false,
serverProtocol: "",
ts6ApiKey: "",
serverPassword: "",
});
expect(botDb.deleteBotInstance("bot1")).toBe(true);
+11 -3
View File
@@ -6,7 +6,7 @@ export interface PlayHistoryEntry {
songName: string;
artist: string;
album: string;
platform: "netease" | "qq" | "bilibili";
platform: "netease" | "qq" | "bilibili" | "youtube";
coverUrl: string;
}
@@ -28,6 +28,8 @@ export interface BotInstance {
serverProtocol: string;
/** API key for TS6 HTTP Query */
ts6ApiKey: string;
/** Password to join the TS server (server password) */
serverPassword: string;
identity?: string;
}
@@ -53,6 +55,9 @@ function migrateSchema(db: Database.Database): void {
if (!names.includes("ts6ApiKey")) {
db.exec("ALTER TABLE bot_instances ADD COLUMN ts6ApiKey TEXT NOT NULL DEFAULT ''");
}
if (!names.includes("serverPassword")) {
db.exec("ALTER TABLE bot_instances ADD COLUMN serverPassword TEXT NOT NULL DEFAULT ''");
}
}
function initTables(db: Database.Database): void {
@@ -80,6 +85,7 @@ function initTables(db: Database.Database): void {
autoStart INTEGER NOT NULL DEFAULT 0,
serverProtocol TEXT NOT NULL DEFAULT '',
ts6ApiKey TEXT NOT NULL DEFAULT '',
serverPassword TEXT NOT NULL DEFAULT '',
identity TEXT
);
`);
@@ -101,8 +107,8 @@ export function createDatabase(dbPath: string): BotDatabase {
`);
const upsertInstance = db.prepare(`
INSERT INTO bot_instances (id, name, serverAddress, serverPort, nickname, defaultChannel, channelPassword, autoStart, serverProtocol, ts6ApiKey, identity)
VALUES (@id, @name, @serverAddress, @serverPort, @nickname, @defaultChannel, @channelPassword, @autoStart, @serverProtocol, @ts6ApiKey, @identity)
INSERT INTO bot_instances (id, name, serverAddress, serverPort, nickname, defaultChannel, channelPassword, autoStart, serverProtocol, ts6ApiKey, serverPassword, identity)
VALUES (@id, @name, @serverAddress, @serverPort, @nickname, @defaultChannel, @channelPassword, @autoStart, @serverProtocol, @ts6ApiKey, @serverPassword, @identity)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
serverAddress = excluded.serverAddress,
@@ -113,6 +119,7 @@ export function createDatabase(dbPath: string): BotDatabase {
autoStart = excluded.autoStart,
serverProtocol = excluded.serverProtocol,
ts6ApiKey = excluded.ts6ApiKey,
serverPassword = excluded.serverPassword,
identity = excluded.identity
`);
@@ -148,6 +155,7 @@ export function createDatabase(dbPath: string): BotDatabase {
autoStart: r.autoStart === 1,
serverProtocol: r.serverProtocol ?? "",
ts6ApiKey: r.ts6ApiKey ?? "",
serverPassword: r.serverPassword ?? "",
identity: r.identity ?? undefined,
}));
},