Adds src/music/spotify/webapi.ts (client-credentials token, catalog
mappers, 429 retry) + tests, and threads the new "spotify" platform id
through the type unions in provider.ts and database.ts. Also widens the
downstream QueuedSong.platform union (audio/queue.ts) and the
getProviderFor parameter (bot/instance.ts) so tsc --noEmit stays clean;
these two are the necessary call-site fixes for the new union member.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a self-contained Kugou provider (bilibili-style: direct API calls, no
embedded API server, no new npm dependency) plus full backend + WebUI wiring.
Provider (src/music/kugou.ts): search, song-url (with device registration),
lyrics (KRC decode), song detail, playlist, album, personal FM, QR login +
cookie persistence, and quality. Request signing / crypto / KRC decoding are
ported from the MIT-licensed MakcRe/KuGouMusicApi using Node's built-in
crypto and zlib (no third-party crypto packages).
Wiring: the "kugou" platform is threaded through the provider contract, queue,
play-history, bot instance/manager dispatch (getProviderFor + the -k command
flag), index/server composition, the music/player/auth routers (unified
/search/all, /quality, the platform coercions, QR login), the cookie store,
and the WebUI (search source tab + badge, SongCard badge, brand token, and a
Kugou QR/cookie login card in Settings).
Verified live during development: search, lyrics, and album playback resolve
correctly. NOT verifiable in CI (Kugou anti-bot blocks the build host's IP):
play-URL resolution, QR login, and VIP audio — these are built faithfully to
the reference and need end-to-end testing on a non-flagged IP / a Kugou
account. See the header comment in kugou.ts.
Includes src/music/kugou.test.ts (mappers + KRC→LRC). An adversarial review
pass fixed: pagination truncating on filtered counts, an ms/seconds duration
heuristic, dfid soft-fail caching, the /v5/url random-dfid fallback, the FM
body identity, and an unguarded nickname decode.
Co-Authored-By: Claude Opus 4.8 (1M context) <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)
The sliding-refresh branch in validateAndTouch hardcoded SESSION_TTL_MS
(7d) for all roles, so a guest session created with GUEST_SESSION_TTL_MS
(1d) was wrongly bumped to 7d on the first touch after the touch
interval. Derive the touch TTL from row.role instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
The by-id user-management handlers use findById, which has no role
filter, so supplying the synthetic GUEST_USER_ID let an admin delete,
re-role, reset-password, and read/write permissions on the shared guest
principal (privilege-escalation / DoS / credential-login holes).
- web/api/users.ts: 404-guard every :id handler against GUEST_USER_ID
(DELETE, reset-password, role, GET/PUT permissions).
- data/users.ts: defense-in-depth — setRoleIfNotLastAdmin and
deleteUserIfNotLastAdmin return "not_found" for any role=guest row.
- web/api/session.ts: wrap POST /guest createSession in try/catch so a
missing guest row yields 503 instead of an unhandled 500.
- Tests: data-layer guest-protection + users-router 404 by-id guards.
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>
Conflict resolution + cross-PR integration:
- player.ts: kept #88's POST /:botId/fm route AND gated it with
requirePermission('player.control') so the new control endpoint honors #80's
permission model (it was added without gating).
- bot.ts: kept #81's relocated /settings routes (the relocation fixes the GET
/settings shadow bug) and dropped #80's now-duplicate bottom copy; gated
POST /settings with requirePermission('bot.manage').
- Navbar.vue: composed #82's dedicated-link scope with #80's permission filter —
displayedBots is now the INTERSECTION (scope ∩ controllable allow-list).
- database.ts: kept BOTH new table sets (#87 favorite_playlists + #80
user_permissions/user_bot_access).
- bot.test.ts: updated to createRequireAuth(sessions, permissions) for #80's new
two-arg signature.
#80 review fixes (credential exposure / IDOR, adversarially verified):
- GET /:id/config now requires bot.manage + bot access AND redacts ts6ApiKey +
identity from the response (was readable by any authenticated member).
- GET /:id and GET /:id/avatar now require bot access (were ungated read oracles).
Adds an admin/member role to WebUI auth. /api/users and /api/audit
are now gated by a requireAdmin middleware; all other authenticated
endpoints accept both roles. Schema migration defaults all existing
users to admin to preserve access. POST /api/users defaults new users
to member; first-run setup always creates an admin. Adds PATCH
/api/users/:id/role with last-admin demotion and deletion guards.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Library.vue: use Song type from store so SongCard's strict platform
union accepts the data (was platform: string, broke after merge tightened
SongCard prop type).
database.test.ts: switch toEqual -> toMatchObject so getBotInstances()
returning extra profile_* schema columns no longer fails the assertion.
Co-Authored-By: Claude Opus 4.7 (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
Add BotProfileManager that updates the bot's TeamSpeak presence when
songs change: album cover as avatar, song info in nickname, away status
toggled on stop/play. Each feature is independently configurable via
REST API and persisted to the database. Permission-safe — features that
fail due to insufficient server permissions are silently disabled until
reconnect. Description falls back to nickname display on TS3 (only
supported via TS6 HTTP Query).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: on Docker restart, loadSavedBots() unconditionally connected
ALL saved bots regardless of autoStart flag, causing a rapid
connect/disconnect loop. Additionally, bot identities were regenerated
on every restart, causing TS server conflicts with stale sessions.
Changes:
- loadSavedBots() now only auto-connects bots with autoStart=true
- startBot/stopBot persist autoStart state so restart behavior matches
user intent
- Bot TS3 identity is persisted to database and reused across restarts
- Database schema migrated to include identity column
- TS3Client.connect() cleans up existing connection before reconnecting
- Stagger bot connections by 1s to avoid overwhelming the TS server
https://claude.ai/code/session_01L2kEV2M1QFWMCyPtLU5LgC
Critical:
- Persist serverProtocol/ts6ApiKey in database schema so TS6 config
survives restarts (added columns + manager save/load)
Medium:
- Clear udpErrorTimer on disconnect to prevent memory leak and stale
log messages from firing after teardown
- Guard against double connect() by disconnecting old client first
- Add settled guard in TS6HttpQuery.request() to prevent double
reject when both res error and req error fire
- Add res.on("error") handler to TS6HttpQuery response stream
Low:
- Cap probeTS3Query banner buffer at 256 bytes to prevent memory abuse
from non-TS3 services sending large data on port 10011
- Remove unnecessary EventEmitter inheritance from TS6HttpQuery
- Update database test fixtures with new serverProtocol/ts6ApiKey fields
https://claude.ai/code/session_016WhH58avUD9xy2dgADJgTh
Implement BiliBiliProvider for video-as-audio playback using direct
BiliBili API calls (search, video info, DASH audio URL extraction).
Add QR code login support and cookie persistence. Update FFmpeg to
send Referer header for BiliBili CDN URLs. Extend platform union type
to "netease" | "qq" | "bilibili" across all interfaces. Add -b flag
for chat commands and B站 badge in web UI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>