With several people sharing one bot, personal FM always followed the one
account the bot was logged in with. Each signed-in (non-guest) web user
can now scan a QR code under Settings → 账户 to link their own NetEase
account; FM they start from the WebUI then comes from their account.
- user_music_cookies table (per user + platform, dropped with the user).
- NeteaseProvider.pollQrLogin returns the cookie without storing it, so
a personal login can never replace the bot's shared account;
checkQrCodeStatus is now built on it. withCookie gives a view bound to
another account.
- /api/me/music/netease: status / qrcode / qrcode/status / unlink, acting
only on req.user. The cookie never leaves the server.
- POST /api/player/:botId/fm uses the caller's linked account for
NetEase. Songs still resolve through the shared provider when played.
TeamSpeak chat !fm keeps using the shared account: chat users are not
tied to web accounts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
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.
- Replace single-page layout with pill-slider tabs (单曲/专辑/歌单) under
the search box, showing only one category at a time with result counts
- NetEase album/playlist search limit raised from 5 to 10 to match QQ
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add mapNeteaseAlbums pure helper and wire cloudsearch?type=10 as the third Promise.all arm in search(), replacing the hardcoded albums:[].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The /playlist/:id/detail route was hardcoded to call the NetEase API
path /playlist/detail on whatever provider was selected. For QQ this
hit a non-existent path, so clicking into any QQ playlist showed
"歌单不存在或加载失败".
Replace the platform-specific hack in the route handler with a proper
getPlaylistDetail method on MusicProvider. Implement it for NetEase
(porting the existing /playlist/detail logic) and QQ (calling
/getSongListDetail and reading from response.cdlist[0]).
Pre-existing bug exposed by the QQ source tab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add backend endpoints for daily recommended songs, personal FM, user
playlists, and playlist detail. Extend NeteaseProvider and MusicProvider
interface with getDailyRecommendSongs and getUserPlaylists. Rewrite Home
page with FM card, daily recommendations grid, and user playlists section.
Fix Playlist view to fetch detail and songs from separate endpoints.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Auth router accepts CookieStore and persists cookies on login
- NeteaseProvider passes timestamp to prevent cached QR responses
- QR image from server used directly (qrimg field)
- Cookie saved to disk on confirmed QR scan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>