Add three default-off capabilities that stop the play queue from being lost,
all gated behind admin/independent toggles so existing behavior is unchanged
until an operator opts in:
- Named save/load of queues (Feature 1): new saved_queues table (per-user +
reserved __shared__ owner, capped at 50 queues / 1000 songs, JSON song blob
that degrades to empty on corruption); /api/saved-queues router (list/save/
load/delete with ownership 404s, inert 403 when disabled); chat commands
!save / !load [-a] / !queues; BotInstance.loadSavedQueue (replace/append).
- Auto-restore live queue across restart (Feature 2): PlayQueue.snapshot/restore,
queue_state table (one row per bot), a debounced snapshot writer driven off
stateChange, and restore+resume on connect. Cancels the pending snapshot on
disconnect so a stale write can't wipe the row a restart must restore.
- playKeepsQueue (Feature 3): BotInstance.playSingleSong funnels chat !play and
the web /play-song route through one place; when enabled a single-song play
inserts-after-current and jumps instead of clearing the queue.
Config gains savedQueuesEnabled + playKeepsQueue (both default false, strict-
coerced on load like spotify.enabled); the settings API round-trips them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Runtime playback settings were kept only in memory (AudioPlayer.volume,
PlayQueue.mode, each provider's quality field), so restarting the bot reset
them to defaults and users had to re-tune volume and quality every time (#125).
Persist and restore them via the repo's existing storage:
- Volume and play mode are per-bot, stored on new bot_instances columns
(volume, play_mode) with a schema migration; restored when the instance is
(re)built, written by cmdVol / cmdMode which every entry point (chat command,
WebUI, REST) funnels through. Volume and mode are written independently so a
transient !fm/!artist mode switch never overwrites the user's saved !mode.
- Per-provider audio quality is global (shared providers), stored in a new
config.json `audioQuality` block; applied to the providers at startup and
re-snapshotted on POST /api/music/quality.
Queue, current song, progress and FM/artist sessions stay ephemeral.
Adds tests for config sanitize/round-trip, DB player-settings + migration,
cmdVol/cmdMode persistence + construction-time restore, and quality persistence
through the REST endpoint. Documents the behavior in the README.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Revert the jellyfin-only default introduced by PR #123 so upgrading
users keep their online sources; Jellyfin becomes opt-in:
- default enabledProviders is now the online set (netease/qq/bilibili/
youtube/kugou); defaultPlatform() uses a fixed priority order
(netease -> qq -> kugou -> jellyfin -> bilibili -> youtube) instead
of jellyfin-first, so chat/REST/WebUI default to netease again
- Settings: Jellyfin card is always visible with a new enable toggle
(its enabled bit is enabledProviders membership); guards against
clobbering other providers before the list loads
- Setup wizard: saving the Jellyfin step auto-enables the source when
a server URL was entered
- Search/player store fallbacks flip from jellyfin to netease; !help
no longer hardcodes Jellyfin lines
- tests: update default-platform assertions, add coverage for the new
default set, legacy configs without enabledProviders, priority
order, and explicit jellyfin-only configs
- README: reframe Jellyfin as optional (badges, command table, quality
tiers, dedicated section, changelog), document the enabledProviders
default and the v1.10.0 jellyfin-only window fix, credit @ItsEricRao
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New guest flag playCollection (default OFF), gates play-playlist/play-album
- Keeps playNow's non-destructive semantics intact (Play All clears the queue)
- Admin-toggleable in Settings → 游客模式; default-off, backward-compatible
- Frontend: gate the 播放全部 button on the flag + surface 403 as a toast
instead of failing silently (the silent-failure half of the issue)
loadConfig now sanitizes guestMode the same way the write path does: bots is
coerced to "all" | string[] (numbers/objects/missing fall back to the default
"all"), and permissions are rebuilt from defaults with each known flag
strict-coerced to a boolean so a hand-edited/legacy/corrupt config.json can no
longer crash the gate or leak garbage index keys. The authorize guest gate now
uses === true instead of a truthy check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auto-pause within the first seconds of playback (and re-pause after a manual
play) whenever a listener is actually in the channel.
Root cause (confirmed live against a TS3 server): the full-client
`clientlist -uid -away -voice -groups` command TIMES OUT when other clients are
present in the bot's channel. `getClientsInChannel()` catches the error and
returns `[]`, so the occupancy callers computed `userCount = [].length - 1 = -1`,
which `decideOccupancyAction` reads as `-1 <= 0` → "channel empty" → pause. With
the bot alone, clientlist succeeds (returns just the bot), so the bug only
surfaced when someone was listening — exactly the report.
Fix: a connected bot is always a member of its own channel, so a valid query
returns >= 1 (itself). A length of 0 therefore means the query FAILED, not that
the channel is empty. New pure helper `occupancyFromClientList()` maps a
0-length result to `null` ("occupancy unknown"); `refreshOccupancy()` and the
30s idle poller skip the auto-pause / idle-disconnect decision when the count is
unknown instead of mis-reading it as empty. This also removes a latent
false-positive idle-disconnect on the same failed query.
Also default `autoPauseOnEmpty` to OFF (occupancy detection is unreliable on
some servers); users can opt in from Settings.
Verified live with two clients in one channel: clientlist returns 0 → helper
returns null → no false pause (control: bot alone returns 1 → 0 others, normal).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CONFIG_PATH resolved to ROOT_DIR/config.json (/app/config.json in Docker), but only
DATA_DIR (/app/data) is the mounted volume — every other artifact (DB, cookies, logs,
avatars) already lives under DATA_DIR. So on first run the default config was written
into the ephemeral image layer (never appearing in the volume), and a manually-placed
data/config.json was ignored because the bot read/wrote the root path.
- Move CONFIG_PATH to DATA_DIR/config.json so it lands in the volume and manual edits
take effect.
- Add migrateLegacyConfig(): one-time move of an existing root-level config.json into
the data dir, so existing local installs keep their settings (no silent reset).
- Tests for first-run persistence + the three migration cases.
- README directory tree updated to data/config.json.
Fixes#86
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "复制专属链接" button silently failed on public-IP HTTP deployments
because navigator.clipboard requires a secure context. Now the link is
always revealed in a modal with a read-only input (select-all on focus),
so users can copy manually even when clipboard APIs and execCommand both
fail. The dialog still tries to auto-copy when possible.
Also adds a publicUrl config option that overrides window.location.origin
for link generation (useful behind reverse proxies / with custom domains),
exposed via GET /api/config/public-url, and a trustProxy flag so Express
honors X-Forwarded-* when fronted by nginx/Caddy/Cloudflare.
https://claude.ai/code/session_019FSX3S3UUcKEYWanYmoqUv
Prevents EADDRINUSE crash that killed the entire app when port 3200
was still occupied from a previous run.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>