Commit Graph
451 Commits
Author SHA1 Message Date
saopig1andClaude Opus 4.8 3277736f5d docs(spotify): stage 3 plan — Rust librespot Windows backend (#112)
Drafted from a locked contract + research map, adversarially verified (3 critics).
REQUIRED CORRECTIONS: single shared OAuth threaded to web+controller; auth via the
user's own Developer app + web callback (drop ToS-gray librespot-client + :5588);
resolved-backend status; poll hasPlayed-gating + 204 track-end; Connect error
guarding; verifier-map cleanup. Cross-platform, gated, not e2e-testable (Premium).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 23:00:08 +08:00
saopig1andClaude Opus 4.8 8bd0aae7c3 fix(spotify): loopback-bind sidecar API, recover on sidecar death, per-bot go-librespot ports
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:24:51 +08:00
saopig1andClaude Opus 4.8 9c796ec05c fix(spotify): correct Spotify seek units (s→ms) + gate re-attach on player external state
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:00:56 +08:00
saopig1andClaude Opus 4.8 b5b3585e77 feat(spotify): orchestrate go-librespot backend from BotInstance (Stage 2 Task 7)
Construct one SpotifyController per bot (config.spotify + per-bot work/config
dirs under DATA_DIR, threaded via BotManager + index). resolveAndPlay now
routes spotify: sentinels through controller.ensureStarted/playTrack +
player.playPcmStream (falling back to the Stage-1 message when unavailable),
fences/pauses the sidecar on source transitions, advances via controller
"trackEnded", and delegates pause/resume/stop transport. Correction C4:
no re-attach on a spotify->spotify handoff (playPcmStream once across tracks,
no player.stop() — playPcmStream fences the prior ffmpeg internally); occupancy
auto-pause/resume + updateAutoPause + a new BotInstance.seek() (web seek route)
also delegate to the sidecar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:44:39 +08:00
saopig1andClaude Opus 4.8 179e7c248a fix(spotify): tear down errored backend in SpotifyController (no leak/cross-talk)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:30:13 +08:00
saopig1andClaude Opus 4.8 3a9504500b feat(spotify): SpotifyController backend lifecycle, gating, and event re-emission
Per-bot orchestrator: isAvailable() gates on config.enabled + platform +
binary presence; ensureStarted() starts the backend once (idempotent, retries
on failure); playTrack/pause/resume/seek/stop delegate; getPcmStream() proxies
the backend PCM; re-emits backend trackEnded/metadata. backendFactory injected
for tests (fake backend, no real binary/network).

Correction C3: the controller does not re-emit a raw "error" event (Node's
EventEmitter throws on an unhandled "error"); it logs the backend error and
marks itself not-ready so the next ensureStarted() relaunches. getPcmStream()
returns the backend's single persistent stream (no per-attach PassThrough).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:22:12 +08:00
saopig1andClaude Opus 4.8 6debe23034 feat(audio): add external-PCM mode (playPcmStream) for Spotify sidecar
Adds AudioPlayer.playPcmStream(readable, {onExternalEnd}) that feeds a
long-lived external 48kHz/s16le/stereo Readable into the existing pcmBuffer +
20ms frame loop + Opus encoder without spawning a per-URL ffmpeg. Reuses the
same high/low-water backpressure (pausing/resuming the Readable), suppresses the
underrun trackEnd drain/stall branches while external (emitting a silence frame
to keep the 20ms timeline), tears down externalMode in stop() by DETACHING the
shared readable (remove our data/end/error listeners + pause, never destroy the
sidecar stream) and clearing onExternalEnd, and makes seek() a local no-op in
external mode. The url play() path and all exported pure functions are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:14:15 +08:00
saopig1andClaude Opus 4.8 641da086e5 fix(spotify): GoLibrespotBackend start() cleanup on failure + unhandled-error guard
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:04:42 +08:00
saopig1andClaude Opus 4.8 9d55bea240 feat(spotify): GoLibrespotBackend sidecar (FIFO + ffmpeg PCM + REST/WS)
Implements SpotifyAudioBackend over a go-librespot sidecar: start() mkfifos
the pipe, spawns the FIFO->48k s16le ffmpeg reader BEFORE go-librespot, writes
config.yml, polls the REST /  until ready, then connects the WS event stream.
Maps not_playing/stopped -> trackEnded and metadata -> SpotifyNowPlaying;
play/pause/resume/seek delegate to the REST client. All child_process/fs/REST/WS
seams are injectable so the lifecycle is fully unit-tested without a real binary.

Correction C1: ffmpeg is resolved via getFfmpegCommand() (now exported from
src/audio/player.ts) so the bundled ffmpeg-static fallback is honored in Docker;
the ffmpeg command is overridable via deps.ffmpegCommand for tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:58:13 +08:00
saopig1andClaude Opus 4.8 f2b14b5f00 feat(spotify): add go-librespot REST client + WS event client (Stage 2)
GoLibrespotRestClient wraps axios (injectable via deps.http) for
/player/play|pause|resume|stop|seek, GET /status, GET / ping; ping/getStatus
swallow errors to false/null, mutating ops reject. GoLibrespotEventClient
(EventEmitter) parses {type,data} /events frames and re-emits type with data,
reconnects on close with capped backoff, stop() tears down. TDD with a mock
AxiosInstance and a fake WebSocket (no real binary/network).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:47:44 +08:00
saopig1andClaude Opus 4.8 470a62129a feat(spotify): add SpotifyAudioBackend interface + go-librespot config.yml renderer
- backend.ts: type-only SpotifyAudioBackend contract + track/metadata DTOs
- go-librespot-config.ts: renderConfigYml() hand-built config (pipe/s16le,
  server enabled, interactive OAuth), no yaml dependency
- tests assert exact keys/values and round-trip via a tiny structural parser

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:40:15 +08:00
saopig1andClaude Opus 4.8 32718f0118 feat(spotify): add go-librespot binary resolver + Linux support gate
Mirror youtube.ts findYtDlp/checkYtDlpAvailable (bin/ then PATH,
cache-positive-only availability, reset test hook) and add
isGoLibrespotSupported() Linux gate for the Stage 2 audio backend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:33:42 +08:00
saopig1andClaude Opus 4.8 978e6ee7f2 docs(spotify): stage 2 plan — go-librespot audio backend (#112)
Drafted from a locked interface contract + integration map, then adversarially
verified (3 critics). Includes REQUIRED CORRECTIONS fixing the gapless-handoff
blocker (no stream re-attach on spotify->spotify), detach-not-destroy teardown,
ffmpeg-static resolution, occupancy/seek transport delegation, and unhandled-error
guarding. Linux/Docker-only + gated; not e2e-testable without Premium.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:30:45 +08:00
saopig1andClaude Opus 4.8 77d71fe418 docs(spotify): note intentional Spotify omission from unified /search/all (stage 1)
Spotify tracks are metadata-only until the librespot audio backend lands, so
they are surfaced only from the dedicated Spotify tab, not the default
all-sources search. Conscious decision from the whole-branch review (#112).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 00:32:08 +08:00
saopig1andClaude Opus 4.8 50bdaae26a feat(spotify): frontend plumbing (source tab, badge, auth status)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 00:22:45 +08:00
saopig1andClaude Opus 4.8 e1171dbcfe feat(spotify): expose provider through web music/auth routers
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 00:19:22 +08:00
saopig1andClaude Opus 4.8 9a9f68c446 feat(spotify): wire provider through manager/instance; skip playback sentinel
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 00:13:22 +08:00
saopig1andClaude Opus 4.8 848b3931b8 feat(spotify): config block (disabled by default) + sanitize
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 00:04:31 +08:00
saopig1 828fd21e29 feat(spotify): SpotifyProvider (search/browse; playback sentinel) 2026-07-01 23:59:41 +08:00
saopig1andClaude Opus 4.8 6c16e2d966 feat(spotify): Web API client + catalog mappers, add spotify platform
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>
2026-07-01 23:52:22 +08:00
saopig1andClaude Opus 4.8 b352d71528 docs(spotify): stage 1 implementation plan (metadata + provider + wiring) (#112)
Bite-sized TDD plan for the first shippable increment: Spotify becomes a
searchable/browsable source (Web API), with playback cleanly reporting
"not playable yet". Adversarially verified against the codebase (3 critics)
and fixed: getProviderFor signature, pre-existing config.test.ts imports,
all three BotInstance sites, enabled-gate safety, 429 handling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:04:35 +08:00
saopig1andClaude Opus 4.8 92bdfcb294 docs(spotify): design spec for optional hybrid librespot audio source (#112)
Adds the approved design for a new optional `spotify` MusicProvider that
streams real Spotify audio via a librespot-family sidecar behind one
SpotifyAudioBackend interface (go-librespot on Linux/Docker, Rust librespot
on Windows), with metadata via the Spotify Web API. Disabled by default,
opt-in, Premium-only, ToS-risk warned. Includes verified research appendix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:26:07 +08:00
TIANYAO ZHANG f8743a904f Merge pull request #114 from ZHANGTIANYAO1/fix/qq-api-version-pin
fix(qq): 锁定 @sansenjian/qq-music-api 到 ~2.4.0 并加固启动报错
v1.8.2
2026-06-30 23:26:28 +08:00
saopig1andClaude Opus 4.8 f6e42811a5 chore(deps): update NeteaseCloudMusicApi (pinned) + safe patch/minor bumps
Dependency audit follow-up to the QQ pin:
- NeteaseCloudMusicApi ^4.30.0 → ~4.32.0. Same rationale as @sansenjian/
  qq-music-api: it's an embedded API server loaded via dynamic import, so a loose
  `^` could drift into a breaking minor and silently kill the netease server
  (ECONNREFUSED). Tighten to ~4.32.x. Verified at runtime: server starts on 3001
  and /banner returns 200.
- Lockfile bumps within existing ^ ranges (no API-server risk, so ranges kept):
  better-sqlite3 12.8.0→12.11.1, koa 3.2.0→3.2.1, ws 8.20.0→8.21.0,
  typescript 6.0.2→6.0.3, ts3-nodejs-library 3.5.1→3.5.3, vitest 4.1.4→4.1.9,
  tsx 4.21.0→4.22.4.
- Deliberately NOT bumped (major / needs a migration): bcryptjs 2→3 (password
  hashing), @types/node 25→26, @types/supertest 6→7.

tsc clean; full suite 913 passing under vitest 4.1.9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 23:16:25 +08:00
saopig1andClaude Opus 4.8 08260182a9 fix(qq): pin @sansenjian/qq-music-api to ~2.4.0 + guard startup failures
A loose `^2.2.10` range let npm pull a newer build of the QQ Music API. The
library went ESM in 2.3.x: an ESM-only 2.3.0/2.3.1 throws ERR_REQUIRE_ESM on
load, so the embedded server never binds port 3200 and every QQ request
(including /getQQLoginQr) fails downstream with ECONNREFUSED — the QR code never
appears and cookies look broken.

- Pin to `~2.4.0` (verified: loads via the bot's native ESM import, exposes the
  Koa app, and every endpoint qq.ts calls returns the exact shapes it parses —
  QR, recommend, lyric, play, playlist detail). Blocks the broken 2.3.0/2.3.1
  and any future 2.5 migration. NOTE: 2.4.x requires Node >=20.17 (or >=22.9).
- Add describeQqApiStartupError(): on startup failure, log an actionable error
  (ERR_REQUIRE_ESM → version-pin hint; engine mismatch → Node-upgrade hint)
  instead of a generic warning, so this is obvious from the logs next time.
- README: troubleshooting entry for "QQ 二维码不弹 / 登录失败 / cookie 无法使用"
  and the version/Node note in the dependency table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 23:05:47 +08:00
TIANYAO ZHANG de4343cfbb Merge pull request #113 from ZHANGTIANYAO1/docs/kugou-readme
docs: 在 README 中补充酷狗音乐音源与登录功能
v1.8.1
2026-06-30 21:36:34 +08:00
saopig1andClaude Opus 4.8 af50d69b00 docs: document Kugou (酷狗音乐) source + login features in README
Kugou shipped in #110 but the README still listed only netease/qq/bilibili/
youtube. Add Kugou throughout:
- tagline, badge, 多平台音源, QR 登录, 歌单管理 (酷狗私人电台 !fm -k + the
  login-gated daily/recommend/user playlists)
- quick-start account login, WebUI page table (FM sources, 三→四平台 search,
  multi-platform login), architecture tree (kugou.ts), dependency table,
  milestones, and a credit to the MIT MakcRe/KuGouMusicApi reference
- command table: !play -k / !search [-k] / !artist -k / !fm -k (the flags that
  actually route to Kugou; not !playlist -k — Kugou search returns no playlists)

Also fix the in-bot `!search` usage string to include -k so it matches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 21:36:00 +08:00
TIANYAO ZHANG e1439a5899 Merge pull request #110 from ZHANGTIANYAO1/feat/kugou-provider
feat: 增加酷狗音乐 (Kugou) 音源支持 (closes #69)
v1.8.0
2026-06-30 21:15:12 +08:00
saopig1andClaude Opus 4.8 5ae5168b6b fix(web): keep the volume slider draggable under the per-frame progress loop (#111)
The 60fps requestAnimationFrame progress clock (#107) re-renders the player
every ~16ms, and Vue re-applied `el.value = storeVolume` on a range input each
time — snapping the thumb back to the stale store value mid-drag (un-draggable
on desktop, janky on mobile).

Extract the decoupling into a useDecoupledSlider composable used by both the
desktop (Player.vue) and mobile (App.vue) sliders: a local display ref tracks
the native drag via @input (so the bound value always matches the element), the
store is committed only on @change (release), and an onRelease safety-net
(pointerup/pointercancel/blur) clears the dragging guard even when the browser
skips `change` (value released at its start point). External/store changes still
flow into the display except while dragging. Adds a regression test for the
no-snap-back invariant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 21:12:05 +08:00
saopig1andClaude Opus 4.8 4cb1da29d4 fix(web): render login QR codes dark-on-light so scanners can read them
The QR images used theme-aware colours, so in the default dark theme they were
rendered light-on-dark (inverted). Many in-app scanners — notably the Kugou
music app — cannot decode an inverted QR, so the code looked fine on screen but
silently failed to scan. Force standard dark-on-light regardless of theme; the
white quiet-zone frames it cleanly in dark mode anyway. Affects all platforms'
QR login (netease/qq/bilibili/kugou).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 21:11:53 +08:00
saopig1andClaude Opus 4.8 f9caea6c79 feat(kugou): add login-gated discovery (daily/recommend/user playlists, FM) + covers
Implements the NetEase-parity login features for the Kugou provider now that
QR login works:
- getDailyRecommendSongs (每日推荐), getUserPlaylists (我的歌单), and a real
  getRecommendPlaylists (推荐歌单, was a stub) ported from the reference API.
- mapKugouSong now extracts cover art per endpoint (sizable_cover / cover /
  trans_param.union_cover, resolving the {size} template) — Kugou songs had no
  artwork before.
- Fix the playlist-song shape (combined "歌手 - 歌名" in `name`, mixsongid as the
  audio id) so opened playlists show real titles instead of 未知歌曲.
- New defensive playlist mappers keyed on global_collection_id (the only id
  getPlaylistSongs can open); dedup user playlists in case the list endpoint
  ignores pagination; firstStr() so an empty-string field can't mask a real one.

Frontend wires Kugou as a third home-discovery source (Source type, store
caches/auth, availableSources, fetchHomeData, Home FM card + source tabs,
SourceTabs label, persisted-tab whitelist). SourceTabs now highlights the
fallback-corrected source so the active tab shows when a logged-out source was
persisted (newly possible with 3 sources).

Adds kugou.test.ts coverage for the new mappers, the cover/empty-string and
playlist-shape handling, and id openability.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 21:11:45 +08:00
saopig1andClaude Opus 4.8 a12c419dd2 feat(music): add Kugou (酷狗音乐) as a music source (#69)
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>
2026-06-30 18:55:41 +08:00
TIANYAO ZHANG af326a37b7 Merge pull request #109 from ZHANGTIANYAO1/fix/player-elapsed-reactivity
fix(web): 播放器时间每帧更新、歌词同步 (closes #107)
v1.7.3
2026-06-30 17:26:46 +08:00
saopig1andClaude Opus 4.8 45f5d236c1 fix(web): tick player time every frame and keep lyrics in sync (#107)
`store.elapsed` is a Pinia getter (a cached Vue computed) that interpolates
with `Date.now()`. Because `Date.now()` is not a reactive dependency, the
computed only re-ran on WebSocket pushes / the 3s server poll, so the bottom
progress bar jumped ~3s at a time and lyric highlighting lagged ~half a line —
even though the consumers read it from a 60fps requestAnimationFrame loop.

Add a pure `interpolateElapsed()` helper and a non-cached `liveElapsed()` store
action. The per-frame consumers now call `liveElapsed()` so the value advances
every frame instead of returning a frozen cache:
- web/src/components/Player.vue  (desktop progress bar, rAF)
- web/src/App.vue                (mobile progress bar, rAF)
- web/src/views/Lyrics.vue       (lyric highlight, 500ms interval)

pause() now freezes at the live value rather than a possibly-stale cached one.
The `elapsed` getter is refactored onto the same helper (behaviour unchanged).

Adds web/src/stores/elapsed.test.ts covering the time-advancing interpolation,
paused freeze, no-anchor, and duration-clamp cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 16:17:53 +08:00
TIANYAO ZHANG ea6820204d Merge pull request #108 from Fa1nttt/feature/local-audio-upload
feat: add local audio upload playback 增加本地音频上传播放功能
v1.7.2
2026-06-30 16:03:32 +08:00
saopig1andClaude Opus 4.8 e849db2286 fix(local-audio): reference-aware cleanup, upload quota, stricter validation
Uploaded local files were deleted whenever a track left the current slot,
with no check on whether the file was still needed — causing data loss in
several flows. Replace with reference-aware cleanup: a file is deleted only
once it has been played AND is no longer referenced by ANY bot's queue
(BotManager.getReferencedLocalSongIds wired into the provider via
setInUseResolver), with the sweep run AFTER each queue mutation.

Fixes:
- play-song replay no longer deletes the file it is about to play
- loop / repeat-all / prev no longer destroy uploads mid-cycle
- a shared upload queued on multiple bots is not deleted while still in use
- !play / play-playlist / play-album clean the whole replaced queue, and an
  empty/failed playlist/album load keeps the previous queue + files intact
- bound disk use with an upload quota (evict oldest UNREFERENCED files)
- validate uploads by extension against the audio whitelist (never trust the
  client Content-Type); the stored extension is always a known audio type

Deletion now unlinks the file FIRST and drops the record only on success,
with a bounded non-blocking retry for briefly-locked files (Windows/ffmpeg),
so a failed unlink never orphans a file or diverges index.json. The quota
never evicts the just-uploaded file, and long filenames keep their extension.

Adds src/music/local.test.ts covering the cleanup lifecycle, quota eviction,
and upload validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:58:22 +08:00
Fa1nttt e12cbf8863 feat: add local audio upload playback 2026-06-30 14:08:42 +08:00
TIANYAO ZHANG 4e148302fc Merge pull request #106 from ZHANGTIANYAO1/fix/guest-play-collection
fix(guest): allow Play All for guests via a dedicated playCollection permission (#103)
v1.7.1
2026-06-29 17:39:50 +08:00
saopig1 9c861f487d docs: add playCollection to the guest-permission table (#103) 2026-06-29 12:16:17 +08:00
saopig1 70c0273ae7 fix(guest): add playCollection permission so guests can Play All playlist/album (#103)
- 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)
2026-06-29 12:12:47 +08:00
TIANYAO ZHANG e2fa288f48 Merge pull request #105 from Slldyd2077/feat/song-vip-flag
feat: expose vip flag & trial duration on Song for trial-only playback
2026-06-29 11:59:08 +08:00
TIANYAO ZHANG 59a9e742c8 Merge pull request #104 from ZHANGTIANYAO1/feat/ts-command-permissions
feat: TeamSpeak chat-command permission control (adminGroups)
2026-06-28 23:41:34 +08:00
saopig1 31d3830791 test(ts-protocol): smoke-test getClientServerGroups query string + client_servergroups parse 2026-06-28 23:40:30 +08:00
Slldyd2077 d1bd010260 Merge remote-tracking branch 'upstream/main' into feat/song-vip-flag
# Conflicts:
#	src/bot/instance.ts
2026-06-28 21:31:17 +08:00
Slldyd2077 fbb127a86d feat: resolve trial-only playback via trialDuration/effectiveDuration
VIP songs for non-VIP accounts return a ~30s trial fragment. The player used the full duration for isNearEnd, so the trial end didn't trigger auto-advance (~60s stall), and currentSong.duration stayed full, leaving the UI progress stuck.

- provider.ts: SongUrlResult {url, trialDuration?}; getSongUrl signature
- netease.ts: parseNeteaseTrial (freeTrialInfo start/end in seconds) + getSongUrl
- qq.ts: parseQqTrial (isTryout/tryEnd) + getSongUrl
- bilibili/youtube: getSongUrl returns {url}
- instance.ts: resolveAndPlay uses effectiveDuration = trialDuration ?? duration -> nearEnd at trial end -> native auto-advance; BotStatus.effectiveDuration
- VIP account: freeTrialInfo absent -> full duration -> full playback (no toggle)

Backward compatible (optional fields; getSongUrl has a single caller, updated).
Tests: parseTrial assertions (seconds/alias/ms-fallback). 14 pass.
2026-06-28 21:06:52 +08:00
Slldyd2077 7b2bd0ea6a feat: expose vip flag on Song for trial-only detection
Add optional vip?: boolean to the Song interface so downstream clients
(e.g. PowerfulTS) can mark copyright-restricted songs that non-VIP users
can only play as a trial fragment, before playback starts.

- provider.ts: add optional vip?: boolean (backward compatible)
- netease.ts: extract mapNeteaseSongs() pure fn; map fee to vip
  (1=VIP, 4=album-only). fee=8 (free low-quality) is excluded because
  it plays in full, just at lower quality.
- qq.ts: mapQqSongs() maps pay.payplay/paytrackprice to vip (one fix
  covers all callers); getDailyRecommendSongs inline mapping too.
- tests: vip mapping assertions for netease fee (1/4=vip, 0/8=free) and
  qq pay fields.
2026-06-28 17:12:57 +08:00
saopig1andClaude Opus 4.8 8e5e9c810e fix(bot): resolve sender server groups live + server-wide for the admin-command gate
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:51:06 +08:00
saopig1 e104093614 fix: sanitize adminGroups on config load + final-review cleanups 2026-06-26 21:12:56 +08:00
saopig1 b387d6581e docs: TS chat-command permission implementation plan 2026-06-26 20:57:39 +08:00
saopig1 17ab477af6 docs: correct stale adminGroups references now that the feature ships 2026-06-26 20:53:56 +08:00