From 848b3931b8b0bc954bbccad10030f20ae0de5842 Mon Sep 17 00:00:00 2001 From: saopig1 <4x7sw862st@gmail.com> Date: Thu, 2 Jul 2026 00:04:31 +0800 Subject: [PATCH] feat(spotify): config block (disabled by default) + sanitize Co-Authored-By: Claude Opus 4.8 (1M context) --- src/data/config.test.ts | 58 +++++++++++++++++++++++++++++++++++++++++ src/data/config.ts | 39 +++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/src/data/config.test.ts b/src/data/config.test.ts index dee0dcc..3b30d60 100644 --- a/src/data/config.test.ts +++ b/src/data/config.test.ts @@ -205,3 +205,61 @@ describe("adminGroups normalization", () => { expect(loadAdminGroups({ adminGroups: "6" })).toEqual([]); }); }); + +describe("spotify config", () => { + it("defaults are present and disabled", () => { + const c = getDefaultConfig(); + expect(c.spotify).toEqual({ + enabled: false, + backend: "auto", + clientId: "", + clientSecret: "", + deviceName: "TSMusicBot", + bitrate: 320, + }); + }); + + it("loadConfig coerces bad spotify values back to safe defaults", () => { + const dir = mkdtempSync(join(tmpdir(), "cfg-")); + const p = join(dir, "config.json"); + writeFileSync( + p, + JSON.stringify({ + spotify: { enabled: "yes", backend: "bogus", bitrate: 7, clientId: 5 }, + }) + ); + const c = loadConfig(p); + expect(c.spotify.enabled).toBe(false); // non-boolean → false + expect(c.spotify.backend).toBe("auto"); // invalid enum → auto + expect(c.spotify.bitrate).toBe(320); // invalid → 320 + expect(c.spotify.clientId).toBe(""); // non-string → "" + expect(c.spotify.deviceName).toBe("TSMusicBot"); // missing → default + }); + + it("loadConfig preserves valid spotify values", () => { + const dir = mkdtempSync(join(tmpdir(), "cfg-")); + const p = join(dir, "config.json"); + writeFileSync( + p, + JSON.stringify({ + spotify: { + enabled: true, + backend: "librespot", + clientId: "abc", + clientSecret: "def", + deviceName: "MyBot", + bitrate: 160, + }, + }) + ); + const c = loadConfig(p); + expect(c.spotify).toEqual({ + enabled: true, + backend: "librespot", + clientId: "abc", + clientSecret: "def", + deviceName: "MyBot", + bitrate: 160, + }); + }); +}); diff --git a/src/data/config.ts b/src/data/config.ts index 1668fe2..3b708d6 100755 --- a/src/data/config.ts +++ b/src/data/config.ts @@ -9,6 +9,15 @@ export interface GuestModeConfig { permissions: GuestPermissions; } +export interface SpotifyConfig { + enabled: boolean; + backend: "auto" | "go-librespot" | "librespot"; + clientId: string; + clientSecret: string; + deviceName: string; + bitrate: number; +} + export interface BotConfig { webPort: number; locale: "zh" | "en"; @@ -33,6 +42,7 @@ export interface BotConfig { // behind HTTPS-terminating proxies. trustProxy: boolean; guestMode: GuestModeConfig; + spotify: SpotifyConfig; } export function getDefaultConfig(): BotConfig { @@ -69,6 +79,14 @@ export function getDefaultConfig(): BotConfig { playCollection: false, }, }, + spotify: { + enabled: false, + backend: "auto", + clientId: "", + clientSecret: "", + deviceName: "TSMusicBot", + bitrate: 320, + }, }; } @@ -118,11 +136,32 @@ export function loadConfig(path: string): BotConfig { ) : defaults.adminGroups; + const partialSp = (partial.spotify ?? {}) as Partial; + const validBackends = ["auto", "go-librespot", "librespot"] as const; + const validBitrates = [96, 160, 320]; + const spotify: SpotifyConfig = { + enabled: partialSp.enabled === true, + backend: (validBackends as readonly string[]).includes(partialSp.backend as string) + ? (partialSp.backend as SpotifyConfig["backend"]) + : defaults.spotify.backend, + clientId: typeof partialSp.clientId === "string" ? partialSp.clientId : defaults.spotify.clientId, + clientSecret: + typeof partialSp.clientSecret === "string" ? partialSp.clientSecret : defaults.spotify.clientSecret, + deviceName: + typeof partialSp.deviceName === "string" && partialSp.deviceName.trim() + ? partialSp.deviceName + : defaults.spotify.deviceName, + bitrate: validBitrates.includes(partialSp.bitrate as number) + ? (partialSp.bitrate as number) + : defaults.spotify.bitrate, + }; + return { ...defaults, ...partial, adminGroups, guestMode: gm, + spotify, }; } catch { return defaults;