Merge remote-tracking branch 'origin/main' into feat/issue-119-saved-playlists

# Conflicts:
#	src/data/config.ts
This commit is contained in:
saopig1 committed 2026-07-17 11:09:04 +08:00
commit c8dacebc45
21 files changed
+1163 -86

No files matched your search

+80 -7
View File
@@ -38,6 +38,22 @@ export interface JellyfinConfig {
userId: string;
}
/**
* Per-provider audio quality (音质), persisted so a restart keeps the user's
* choice instead of resetting each provider to its in-memory default (#125).
* The values are the same strings the WebUI/REST `POST /api/music/quality`
* endpoint sends and each provider's setQuality() accepts; on startup they are
* replayed onto the (shared, process-wide) providers. Providers ignore/normalize
* unknown values, so a stale/hand-edited entry can never break playback.
*/
export interface AudioQualityConfig {
netease: string;
qq: string;
bilibili: string;
kugou: string;
jellyfin: string;
}
/**
* Providers gated by `enabledProviders`. Not listed here:
* - "local" → governed by the existing `localAudioEnabled` flag
@@ -61,15 +77,24 @@ export function isProviderEnabled(config: BotConfig, platform: string): boolean
}
/**
* The default platform for !play/!add/!playlist/!album and all REST/WebUI calls:
* the first enabled provider in a fixed priority order (netease with the default
* config; jellyfin ranks after the online music platforms because it is an
* opt-in source, but ahead of the video sites for users who run it as their
* only music library). Falls back to "netease" when nothing is enabled so
* callers always get a provider — the enabled-gate then produces the friendly
* error.
* The default platform for !play/!add/!playlist/!album and all REST/WebUI calls.
*
* An explicit user preference (`config.defaultPlatform`) wins whenever it points
* at a source that is currently enabled — this lets e.g. a Bilibili-loving server
* set B站 as the default so `!play <歌名>` needs no `-b` flag (issue #126). The
* enabled-guard here matters at runtime too: if the operator later disables the
* preferred source, we must fall through instead of returning a dead default.
*
* With no (usable) preference we fall back to the first enabled provider in a
* fixed priority order (netease with the default config; jellyfin ranks after
* the online music platforms because it is an opt-in source, but ahead of the
* video sites for users who run it as their only music library). Falls back to
* "netease" when nothing is enabled so callers always get a provider — the
* enabled-gate then produces the friendly error.
*/
export function defaultPlatform(config: BotConfig): GateableProvider {
const pref = config.defaultPlatform;
if (pref && config.enabledProviders.includes(pref)) return pref;
for (const p of ["netease", "qq", "kugou", "jellyfin", "bilibili", "youtube"] as const) {
if (config.enabledProviders.includes(p)) return p;
}
@@ -115,6 +140,8 @@ export interface BotConfig {
guestMode: GuestModeConfig;
spotify: SpotifyConfig;
jellyfin: JellyfinConfig;
/** Persisted per-provider audio quality (音质), restored on startup (#125). */
audioQuality: AudioQualityConfig;
/**
* Which gateable providers are active (see GATEABLE_PROVIDERS). Default is
* the online sources (NetEase/QQ/Bilibili/YouTube/Kugou); jellyfin is an
@@ -123,6 +150,14 @@ export interface BotConfig {
* API servers must not start (or bind ports 3001/3200) unless enabled.
*/
enabledProviders: GateableProvider[];
/**
* Optional operator-chosen default source for commands/REST/WebUI calls that
* omit a platform (issue #126). When set to an enabled gateable provider it
* overrides the fixed priority order in defaultPlatform(); `null` (the default)
* keeps that priority order. loadConfig cleans stale/unknown/disabled values
* back to null.
*/
defaultPlatform: GateableProvider | null;
}
export function getDefaultConfig(): BotConfig {
@@ -177,7 +212,17 @@ export function getDefaultConfig(): BotConfig {
apiKey: "",
userId: "",
},
// Mirrors each provider's own in-memory default quality; overwritten on
// startup once the user has changed a quality (persisted via #125).
audioQuality: {
netease: "exhigh",
qq: "exhigh",
bilibili: "high",
kugou: "128",
jellyfin: "direct",
},
enabledProviders: ["netease", "qq", "bilibili", "youtube", "kugou"],
defaultPlatform: null,
};
}
@@ -338,6 +383,32 @@ export function loadConfig(path: string): BotConfig {
const savedQueuesEnabled = partial.savedQueuesEnabled === true;
const playKeepsQueue = partial.playKeepsQueue === true;
// defaultPlatform → an explicit operator default (issue #126). Keep it only
// when it names a KNOWN gateable provider that is ALSO currently enabled;
// anything else (unknown value, disabled source, wrong type, missing) becomes
// null so defaultPlatform() falls back to the fixed priority order.
const rawDefault = partial.defaultPlatform;
const defaultPlatformPref: GateableProvider | null =
typeof rawDefault === "string" &&
(GATEABLE_PROVIDERS as readonly string[]).includes(rawDefault) &&
enabledProviders.includes(rawDefault as GateableProvider)
? (rawDefault as GateableProvider)
: null;
// audioQuality → per-provider strings; each field falls back to its default
// when missing/blank/non-string (a hand-edited/legacy config must never smuggle
// a non-string past the gate — the value is fed straight to provider.setQuality).
const partialAq = (partial.audioQuality ?? {}) as Partial<AudioQualityConfig>;
const coerceQuality = (v: unknown, fallback: string): string =>
typeof v === "string" && v.trim() ? v : fallback;
const audioQuality: AudioQualityConfig = {
netease: coerceQuality(partialAq.netease, defaults.audioQuality.netease),
qq: coerceQuality(partialAq.qq, defaults.audioQuality.qq),
bilibili: coerceQuality(partialAq.bilibili, defaults.audioQuality.bilibili),
kugou: coerceQuality(partialAq.kugou, defaults.audioQuality.kugou),
jellyfin: coerceQuality(partialAq.jellyfin, defaults.audioQuality.jellyfin),
};
return {
...defaults,
...partial,
@@ -345,9 +416,11 @@ export function loadConfig(path: string): BotConfig {
guestMode: gm,
spotify,
jellyfin,
audioQuality,
enabledProviders,
savedQueuesEnabled,
playKeepsQueue,
defaultPlatform: defaultPlatformPref,
};
}
}