- Add platform source filter bar (网易云/QQ/B站) above category tabs with
localStorage persistence to remember user preference
- Remove "全部" option, single-source view only
- Increase album/playlist card column-gap to 28px for better spacing
- Sync search query to URL via router.replace so back-navigation from
album/playlist detail pages restores search results
- Fix !album command: add name-based album search (matching !playlist
behavior) so "!album APT." searches by name instead of treating it as ID
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
Album and playlist results now show source tags (网易云, QQ, B站, YouTube)
matching the existing SongCard platform badge style.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User reported that the edit-bot dialog had no avatar option (only
create-bot did). Reuses the same CustomAvatarRow component, which
auto-loads on mount and PUT/DELETEs on change. Each bot's avatar
is bound by botId — independent across bots.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The album detail endpoint intentionally 404s (no /api/music/album/:id/detail
route), so we fall through to the stub built from songs. The album name is
already on every song (Song.album), so use it instead of the literal "专辑".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds AvatarUpload to the create-bot form (PUT on new bot id after POST)
and a CustomAvatarRow per-bot in the profile-toggles section (GET on
mount, PUT/DELETE on user action with initializing guard).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Library / Search / History / Playlist now route the third action to
store.playNextSong. Home is unchanged (it doesn't use SongCard).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surface failures + tighten edges:
1. Toast for /play-song & /play-playlist failures. Backend now returns
{ok, message} (localized in Chinese to match the rest of the UI).
Store stashes a notification on ok=false; new Toast.vue mounted in
App.vue shows it for 3-5s then fades. Clicking the X dismisses
immediately. Sits above the player on desktop and above the mobile
tabbar on phones.
2. QQ collected playlists pagination. fcg_get_profile_order_asset.fcg
returns max 30 per call; we now loop using has_more / short-page
detection up to a 300-playlist hard cap. Single-call users (typical)
exit the loop on the first iteration so no extra requests.
3. getPlayableSongIds chunking. 100 mids per request keeps URL well
under 8KB; chunk-level errors are isolated so a transient blip on
one chunk doesn't poison the whole batch. Returns null only when
every chunk failed (caller falls back to sequential retry).
4. SourceTabs single-source mode now renders a small subdued "网易云"
or "QQ" label instead of vanishing entirely, so the user always
knows which platform's data they're looking at.
5. Hide the "我的歌单 N" count badge when N=0 — Home and Library no
longer show "我的歌单 0" with an empty grid.
6. Auth state change invalidates fetchHomeData cache. Previously, a
user who logged out as account A and into account B within 5
minutes would see A's playlists. Now we always re-check auth at
the top of fetchHomeData and bypass the TTL cache when authStatus
has changed.
Out of scope:
- NetEase analogous batch precheck (per request).
- 60s TS3 UDP idle disconnect — that's the bundled @honeybbq/teamspeak-
client UDP layer kicking when no server packet arrives in 60s. It's
baked in (constant `v=6e4`) and not exposed as an option, and root
cause is server-side or network-layer behavior we can't reach from
here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small reliability fixes from re-auditing:
1. Playlist.vue used Promise.all, so a flaky /detail endpoint would
blank out the whole page even though /songs returned just fine.
Switch to allSettled and synthesize a stub playlist header from
the song list when only detail fails. User can still play the
playlist; just loses the description/cover.
2. sourceTabs.readAll: typeof null === 'object' AND typeof [] ===
'object', so a corrupted localStorage value (e.g. an array) would
be treated as a record and its missing keys would silently fall
back. Reject explicitly so the failure mode is "clean defaults"
instead of "wrong shape that almost works".
3. Settings.vue loadProfileConfig: a 200 response with empty/wrong
body would set profileConfigs[botId] to a falsy/wrong-shape value,
leaving the row stuck on "加载中..." (because the v-if uses
!profileConfigs[id]). Validate the shape; surface "响应格式异常"
so the retry link is reachable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small but real correctness fixes from auditing recent commits:
1. getPlayableSongIds returned an empty Set for both "endpoint failed"
and "succeeded but all unplayable" — the caller couldn't tell which.
Return Set | null now: null = error (fall through to sequential
retry), empty Set = authoritative "all unplayable" (short-circuit
to a clear message instead of wasting 20+ retries).
2. Web store: fetchHomeData unconditionally wrote lastFetchTime even
when every fetch rejected (network blip, server down). That cached
the failure for 5 minutes — user had to hard-reload to recover.
Now only commit lastFetchTime if at least one auth-status call
succeeded.
3. Settings profile section: if GET /profile failed, profileConfigs
stayed undefined and the row showed "加载中..." forever. Track a
per-bot error state and render an inline "加载失败 / 重试" link
so the user can recover without page reload.
Out of scope but documented:
- ein=29 hardcoded in fetchCollectedPlaylists (no pagination yet —
users with 30+ collected QQ playlists get truncated).
- /play-song single-failure UX (returns "Cannot play" message but
frontend ignores; needs a global toast/notification primitive).
- NetEase has no analogous batch precheck (could surface same
"click and wait silent" issue if user has region-restricted NetEase
playlists).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each bot gets a collapsible card with 6 toggles for the TeamSpeak
profile features (avatar, description, nickname, away status, channel
description, now-playing chat message). Most relevant: lets the user
turn off "更新频道描述", which was triggering the "channel edited"
notification sound on every song change. Same goes for the now-playing
chat message.
The two toggles that broadcast a sound to other channel members are
flagged with an inline ⚠️ tag so users notice them.
Wires up to the existing GET/PUT /api/player/:botId/profile endpoints
(no backend changes). Optimistic update with revert on error. Mobile
breakpoint enlarges the switch to a 44x24 touch target.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Reset per-platform fields on fetch failure (was leaving stale data
for up to 5 minutes after the user logged out of NetEase).
- Extract availableSources getter on the store; deduplicate the daily/
user availability computeds in Home.vue and Library.vue.
- Add comment explaining why recommendAvailable always seeds netease.
- Import Source type in SourceTabs.vue from the store instead of
redeclaring locally, removing a future type-drift risk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Recommend playlists, daily songs, and user playlists on Home now show
a [网易云][QQ] tab when both platforms are logged in. Library 我的歌单
gets the same tab. Selection persists per-section in localStorage and
falls back gracefully when the persisted source becomes unavailable
(e.g., user logged out). Removes dead 我的收藏 block from Library that
referenced a non-existent /api/music/user/liked endpoint.
Spec: docs/superpowers/specs/2026-05-06-music-source-tabs-design.md
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>
Add mobile layout (mini player, bottom tab bar, responsive navbar), new
Library view, redesigned bot dropdown with action buttons, and centralize
colors/typography/shadows into CSS variables in variables.scss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Store queue and timing state per-bot instead of globally, move bot
selector from Home page into Navbar dropdown, show bot name badge
in the player bar, and route WebSocket stateChange queues per-bot.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
- New endpoints: play-by-id, add-by-id, play-playlist (load all songs at once)
- Playlist 'Play All' now uses server-side bulk load (no N sequential searches)
- Search/Home/Playlist views use playById instead of search-by-name
- Fixed updateBotStatus timing: capture prev state before mutation
- Early return on song change prevents stale pause/resume logic
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Progress bar reads playStartedAt directly in requestAnimationFrame loop
bypassing Vue reactivity caching for smooth 60fps updates
- Queue panel moved to sibling of player-bar (was trapped inside causing z-index issues)
- Added progress thumb dot on hover
- Click on progress shows 'seek not available' tooltip
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Playlist: add console.error in catch block so load failures are visible
- Player: add progress bar with requestAnimationFrame and time display
- Player: extract toggleQueue function with debug logging for queue panel
- WebSocket: fetch queue on stateChange when not included in event payload
- Lyrics: use CSS transform scrolling and reduce sync to 500ms to fix flicker
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Start @sansenjian/qq-music-api server via dynamic import in api-server.ts
- Rewrite QQ Music provider to use real API endpoints (getSearchByKey,
getMusicPlay, getSongListDetail, getAlbumInfo, getLyric, etc.)
- Cache home page data (recommend playlists, daily songs, user playlists)
in Pinia store with 5-minute TTL to avoid refetching on every mount
- Add unified /api/music/search/all endpoint that queries both Netease
and QQ providers in parallel and returns merged results
- Remove platform toggle from Search.vue; search both platforms at once
- Add platform badge (网易云 / QQ) to SongCard component
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add play mode cycle button (seq/loop/random/rloop) to Player bar
- Wire GET /api/player/:botId/history to query database instead of returning empty array
- Pass database instance from server.ts to createPlayerRouter
- Add back navigation button to Playlist, Lyrics, History, Search, and Settings views
- SongCard already had dblclick-to-play (no change needed)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Player store tracks playStartedAt/pausedElapsed for elapsed time
- Queue component fetches from server when opened
- Lyrics page syncs every 200ms using elapsed time
- Lyrics auto-scroll to active line with smooth animation
- Translation lines shown below main lyrics
- Fade mask on lyrics scroll area
Co-Authored-By: Claude Opus 4.6 (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>
- QR code generated client-side via qrcode library
- Auto-polls backend every 2s for scan status
- Shows waiting/scanned/confirmed/expired states
- Falls back to cookie paste method
- Auth status check on page load
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>