feat: add configurable default music source (#126)

Make the default playback source a user-configurable setting so servers
that mostly play e.g. Bilibili no longer need to type `-b` on every
`!play`. Previously defaultPlatform() always picked the first enabled
provider by a fixed priority order, with no way to override it.

- config: add optional `defaultPlatform: GateableProvider | null`.
  loadConfig sanitizes it — kept only when it names a known provider that
  is also currently enabled, else null. defaultPlatform() returns the
  preference when enabled, otherwise falls back to the fixed priority order.
- POST /api/bot/settings accepts `defaultPlatform` (validated against the
  possibly-updated enabledProviders; null/"" clears it), and reconciles a
  stored default that a new enabledProviders list no longer allows. GET and
  POST responses expose the field.
- WebUI: new "默认音源" section with a source picker; saving refreshes the
  store's default source so it takes effect immediately without a restart.
- Tests: extend config defaultPlatform priority tests and add coverage for
  the settings endpoint and /providers routing.
- README: document `defaultPlatform` in the enabledProviders section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Fable 5 committed 2026-07-16 23:43:39 +08:00
1 parent 051171b019
commit 987513a5f6
7 files changed
+279 -9

No files matched your search

+55
View File
@@ -82,6 +82,61 @@ describe("config", () => {
expect(defaultPlatform(config)).toBe("netease");
});
// --- #126: an explicit operator default source ---
it("defaultPlatform is null by default (follow the priority order)", () => {
expect(getDefaultConfig().defaultPlatform).toBeNull();
});
it("defaultPlatform() honors an explicit, enabled preference over the priority order", () => {
const config = getDefaultConfig();
// Priority would pick netease; a Bilibili-loving server sets B站 instead (#126).
config.defaultPlatform = "bilibili";
expect(defaultPlatform(config)).toBe("bilibili");
});
it("defaultPlatform() ignores a preference whose source is not enabled", () => {
const config = getDefaultConfig();
config.defaultPlatform = "jellyfin"; // opt-in, not enabled in the default config
// Falls back to the fixed priority order (netease)…
expect(defaultPlatform(config)).toBe("netease");
// …until the preferred source is actually enabled.
config.enabledProviders = [...config.enabledProviders, "jellyfin"];
expect(defaultPlatform(config)).toBe("jellyfin");
});
it("loadConfig keeps a valid, enabled defaultPlatform", () => {
const dir = makeTmpDir();
const path = join(dir, "config.json");
writeFileSync(path, JSON.stringify({ defaultPlatform: "bilibili" }));
const config = loadConfig(path);
expect(config.defaultPlatform).toBe("bilibili");
expect(defaultPlatform(config)).toBe("bilibili");
});
it("loadConfig nulls a defaultPlatform that is unknown, disabled, or the wrong type", () => {
const dir = makeTmpDir();
// Unknown provider name.
const p1 = join(dir, "c1.json");
writeFileSync(p1, JSON.stringify({ defaultPlatform: "bogus" }));
expect(loadConfig(p1).defaultPlatform).toBeNull();
// Known provider, but not in enabledProviders.
const p2 = join(dir, "c2.json");
writeFileSync(p2, JSON.stringify({ enabledProviders: ["netease"], defaultPlatform: "bilibili" }));
expect(loadConfig(p2).defaultPlatform).toBeNull();
// Wrong type.
const p3 = join(dir, "c3.json");
writeFileSync(p3, JSON.stringify({ defaultPlatform: 42 }));
expect(loadConfig(p3).defaultPlatform).toBeNull();
});
it("round-trips defaultPlatform through save/load", () => {
const dir = makeTmpDir();
const path = join(dir, "config.json");
saveConfig(path, { ...getDefaultConfig(), defaultPlatform: "qq" });
expect(loadConfig(path).defaultPlatform).toBe("qq");
});
it("respects an explicit jellyfin-only enabledProviders from disk", () => {
const dir = makeTmpDir();
const path = join(dir, "config.json");