Commit Graph
441 Commits
Author SHA1 Message Date
saopig1andClaude Opus 4.8 3802c90d2d fix(audio): smooth, monotonic volume curve (#84)
applyVolume() mapped 0-100 with a two-piece, discontinuous curve: gain =
(vol/100)*0.2 for vol<100 (so the whole 0-99 range only spanned 0..0.198, making
80->99 feel flat) then a raw passthrough at vol===100 (a ~5x jump to full
loudness). That produced the reported dead zone + sudden ear-blast at 100.

Replace it with a single continuous, strictly-monotonic curve
volumeToFactor(v) = 0.2*x + 0.8*x^8 (x = v/100): 0 at 0, exactly 1.0 at 100, no
flat region and no discontinuity, so the slider feels proportional and full
loudness is still reserved at 100. Extracted as an exported pure function and
unit-tested (boundaries, strict monotonicity, dead-zone removal, no jump at 100).

Note: per the maintainer's note on #84 the >80% suppression was intentional
ear-protection; this change makes the upper range (above ~75%) audibly louder
than before in exchange for a proportional slider — applied per maintainer
decision.

Fixes #84

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:21:09 +08:00
saopig1andClaude Opus 4.8 839f777a75 fix(player): recover B站 long-stream playback stalls instead of going silent (#89)
Two issues caused a long BiliBili stream to stop partway (~16 min) and never resume:

1. FFmpeg lacked -reconnect_at_eof. B站 CDN sessions can close the connection
   mid-file (premature EOF); without this flag FFmpeg treats that EOF as
   end-of-input and stops. Added it (HTTP only) so FFmpeg re-issues a Range
   request and finishes the stream.

2. The frame loop only ended a live-but-silent FFmpeg when within 5s of the song
   end (isNearEnd). Far from the end, emptyFrameAttempts grew unbounded, no
   trackEnd was emitted, and audio went permanently silent ('无法继续播放').
   Added a far-from-end stall watchdog (MAX_STALL_ATTEMPTS ~= 60s) via a pure,
   tested shouldEndOnStall() helper, so a genuinely dead stream advances instead
   of hanging — while a transient underrun on a healthy stream is left alone.

Tests: assert -reconnect_at_eof 1 is present (before -i) for HTTP and absent for
local files; shouldEndOnStall covers near-end fast end, far-from-end no-false-skip,
and far-from-end eventual recovery.

Fixes #89

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:46:08 +08:00
saopig1andClaude Opus 4.8 dd6affca6d fix(config): store config.json under the persisted data dir (#86)
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>
2026-06-16 15:33:59 +08:00
saopig1 bea2f92508 Merge PR #80: feat(perm) fine-grained account permissions
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).
2026-06-16 15:05:57 +08:00
saopig1 f19f56a666 fix(favorites): error handling + state hydration + input validation [#87 review]
- addFavorite/removeFavorite now wrap axios in try/catch: a 409 (already favorited,
  common on a stale heart) or 404 resyncs instead of throwing an unhandled promise
  rejection; other errors surface a toast.
- fetchHomeData refreshes favorites BEFORE the TTL cache-return (was appended after
  the early return, so warm-cache loads never refreshed); removed the now-redundant
  trailing call. App.vue onMounted also hydrates favorites so deep-links to Search/
  Playlist show correct hearts.
- favorites API: GET /check rejects non-string (array) query params with 400 instead
  of a 500; POST defaults req.body to {} so a missing JSON body yields the intended 400.
2026-06-16 14:53:18 +08:00
saopig1 140020f63a Merge PR #87: local favorites feature
# Conflicts:
#	web/src/stores/player.ts
2026-06-16 14:50:25 +08:00
saopig1 6e10764d28 fix(qq-fm): guard FM start when offline + reset radar page on re-login [#88 review]
- startFm() now refuses with 'Bot is not connected to TeamSpeak' before mutating the
  queue, so POST /api/player/:id/fm can no longer wipe the queue and flip the bot into
  FM mode while disconnected (the !fm chat command already had this guard).
- The /fm route's success detection also treats 'not connected' as a failure so the
  toast type is correct.
- QQMusicProvider.setCookie() resets radarPage to 1 so a re-login with a different
  account no longer inherits the previous account's radar pagination cursor.
2026-06-16 14:48:01 +08:00
saopig1 9bfe831022 Merge PR #88: feat(qq) QQ Music radar / personal FM stream 2026-06-16 14:45:51 +08:00
saopig1 bbdd4cbc78 fix(autopause): decouple auto-pause toggle from idle-timeout save [#81 review]
The checkbox @change was wired to saveIdleTimeout, which POSTed BOTH idleTimeoutMinutes
and autoPauseOnEmpty: toggling silently committed an unsaved idle edit, and an empty/
non-numeric idle field made the combined POST 400 (errors swallowed), leaving the
checkbox flipped but not persisted. Give the toggle its own saveAutoPause() sending only
the boolean; 保存 now sends only idleTimeoutMinutes.
2026-06-16 14:45:01 +08:00
saopig1 c57cd35f09 Merge PR #81: feat(autopause) pause when bot channel empties 2026-06-16 14:43:54 +08:00
saopig1 1a1f365cf1 fix(scope): clear scope when the scoped bot is removed [#82 review]
removeBotStatus (botRemoved WS frame or admin deleting the scoped bot) left
scopedBotId dangling: isScoped stayed true, displayedBots went empty, and activeBot
silently fell back to bots[0], locking the UI onto a phantom bot. Clear the scope
when the scoped bot disappears.
2026-06-16 14:43:18 +08:00
saopig1 d1544bab42 Merge PR #82: feat(scope) lock UI to a bot via dedicated link 2026-06-16 14:42:32 +08:00
lTinchl e0d17cf404 feat(qq): add radar FM stream 2026-06-06 21:09:57 +08:00
Kun-ovO b2de607391 本地收藏功能 2026-05-31 23:27:02 +08:00
saopig1 355793b7e6 fix(scope): keep mounting if initial navigation errors (parity with old unconditional mount) 2026-05-30 15:25:20 +08:00
saopig1 593b42830c fix(scope): await router.isReady before mount so refreshed ?bot locks the right bot 2026-05-30 15:22:51 +08:00
saopig1andClaude Opus 4.8 463a8e2f8a feat(scope): lock Navbar selector to scoped bot + apply scope on load
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 15:17:01 +08:00
saopig1 88ac7d2a68 feat(scope): dedicated link seeds ?bot scope instead of bare redirect 2026-05-30 15:14:54 +08:00
saopig1 53d28de17e feat(scope): router guard syncs + preserves ?bot across navigation 2026-05-30 15:11:42 +08:00
saopig1andClaude Opus 4.8 ca07ebc3b7 feat(scope): player store scopedBotId + resolveScopedBot helper
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 15:09:03 +08:00
saopig1 bf1fb1fd88 docs(plan): dedicated-link bot scoping implementation plan (#79 items 2,4) 2026-05-30 15:07:26 +08:00
saopig1andClaude Opus 4.8 846fb2c28c docs(spec): dedicated-link bot scoping + refresh fix design (#79 items 2,4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 15:06:09 +08:00
saopig1 34655e5f50 feat(autopause): autoPauseOnEmpty toggle in Settings 2026-05-30 14:58:35 +08:00
saopig1andClaude Opus 4.8 491bc53dec feat(autopause): expose autoPauseOnEmpty via /api/bot/settings
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 14:55:18 +08:00
saopig1 8f5bb26b3a feat(autopause): re-emit client enter/leave/move for instant pause/resume 2026-05-30 14:50:52 +08:00
saopig1andClaude Opus 4.8 4ba4b013b0 feat(autopause): drive pause/resume from channel occupancy in BotInstance
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 14:46:50 +08:00
saopig1 484202e90d feat(autopause): pure occupancy-decision function 2026-05-30 14:44:28 +08:00
saopig1 9f0ac74fbc docs(plan): auto-pause on empty channel implementation plan (#79 item 3) 2026-05-30 14:43:38 +08:00
saopig1andClaude Opus 4.8 51c954993a docs(spec): auto-pause on empty channel design (#79 item 3)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 14:42:13 +08:00
saopig1andClaude Opus 4.8 907a6651f5 fix(perm): access-check before bot-existence (no 403/404 leak); label permissions audit action
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 14:12:04 +08:00
saopig1andClaude Opus 4.8 1ca1ca9d0c test(perm): assert /me capabilities+bots; dry backfill token list
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 14:06:59 +08:00
saopig1andClaude Opus 4.8 d70664067c feat(perm): admin permission editor in user management
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 14:03:54 +08:00
saopig1 5177b41951 feat(perm): gate Queue.vue remove/clear/play-at controls by capability 2026-05-30 14:00:27 +08:00
saopig1 bb86f7e9ed feat(perm): gate idle-timeout + bot-profile settings on bot.manage 2026-05-30 13:56:46 +08:00
saopig1andClaude Opus 4.8 221f7c8dcf feat(perm): hide UI a member lacks capability for
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 13:55:31 +08:00
saopig1 cf76e0f69a feat(perm): frontend session capabilities + can()/canControlBot() 2026-05-30 13:51:34 +08:00
saopig1andClaude Opus 4.8 abf60141d9 feat(perm): one-time backfill of existing members to full access
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 13:47:44 +08:00
saopig1andClaude Opus 4.8 ce15f36e5e feat(perm): admin permissions API + audit + new-member basic tier
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 13:44:12 +08:00
saopig1andClaude Opus 4.8 1f0f162f66 feat(perm): filter GET /api/bot to allowed bots; prune access on bot delete
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 13:38:57 +08:00
saopig1andClaude Opus 4.8 cd6f2c6078 feat(perm): enforce capabilities + bot access on action routes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 13:32:08 +08:00
saopig1andClaude Opus 4.8 696b224f8d feat(perm): load capabilities + bot access onto req.user; expose via /me
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 13:21:24 +08:00
saopig1 7a8666efbb feat(perm): resolvePermissionContext (admin = super-user) 2026-05-30 13:17:06 +08:00
saopig1 f0c979ce71 docs(spec): use 'capabilities' consistently for req.user field 2026-05-30 13:15:44 +08:00
saopig1 d810a2ec0f test(perm): cover requireBotAccess 401 + missing-param cases 2026-05-30 13:15:01 +08:00
saopig1andClaude Opus 4.8 554501cc74 feat(perm): requirePermission + requireBotAccess middleware
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 13:10:39 +08:00
saopig1andClaude Opus 4.8 aaf6ba2ab4 feat(perm): permission store + capability tokens + tables
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 13:05:38 +08:00
saopig1andClaude Opus 4.8 547aaa304e docs(plan): account permissions implementation plan (#79-E)
11 TDD tasks: permission store + tables, requirePermission/requireBotAccess, req.user wiring, route enforcement, bot-list filtering, admin API + audit, one-time member backfill, and frontend gating + permission editor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 12:49:57 +08:00
saopig1andClaude Opus 4.8 f09a589940 docs(spec): fine-grained account permissions design (#79-E)
Capability flags (player.control/player.queue/bot.manage/platform.auth/quality) + per-member bot allow-list, layered under the existing member role; admin is super-user. Backend-enforced via requirePermission/requireBotAccess; existing members backfilled to full on upgrade, new members get a basic tier.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 12:44:24 +08:00
TIANYAO ZHANG a861b41809 Merge pull request #78 from ZHANGTIANYAO1/feat/shuffle-bag-random-modes
feat(queue): 随机循环改为洗牌袋,每首歌播完一轮再重复 (优化随机循环逻辑)
v1.5.4
2026-05-29 22:16:35 +08:00
saopig1andClaude Opus 4.8 e9b3ba0075 feat(queue): shuffle-bag random modes so every song plays before repeating
随机循环 (rloop) used true random-with-replacement, so some songs repeated constantly while others were starved (issue #70). Both random modes now draw from a shuffle bag: every song plays exactly once per cycle in random order. They differ only at cycle end — 随机 (random) stops, 随机循环 (rloop) reshuffles and continues, excluding the just-played song from the first pick of the new cycle to avoid a back-to-back repeat across the boundary. Songs added mid-cycle stay eligible within the current cycle.

随机's visible behavior is unchanged (it already avoided in-cycle repeats); the two branches now share one selection path. Adds shuffle-bag tests (per-cycle permutation, even distribution, no cross-boundary repeat, mid-cycle add).

Closes #70

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 22:08:40 +08:00