Commit Graph
475 Commits
Author SHA1 Message Date
saopig1andClaude Opus 4.7 8cccf2ed24 fix(web): album hero shows real album name from songs[0].album
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>
2026-05-07 20:35:35 +08:00
saopig1andClaude Sonnet 4.6 f6b82b8e21 feat(web): show album + playlist sections in search
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 20:26:03 +08:00
saopig1andClaude Sonnet 4.6 51d7ce61bd feat(web): /album/:id route reusing Playlist view
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 20:24:20 +08:00
saopig1andClaude Sonnet 4.6 20f4cfacea feat(api): /search/all returns albums and playlists
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 20:21:07 +08:00
saopig1andClaude Sonnet 4.6 7d9081efc1 feat(qq): include albums in search results
Add mapQqAlbums pure helper (exported, TDD-covered) and wire a parallel
req_album sub-request (search_type: 8) into search(), populating the
albums field of SearchResult.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 20:20:05 +08:00
saopig1andClaude Sonnet 4.6 a2453784c1 feat(netease): include albums in search results
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>
2026-05-07 20:17:46 +08:00
saopig1andClaude Sonnet 4.6 0f9c7b3c4c feat(web): custom avatar in create-bot + Settings
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>
2026-05-07 20:14:34 +08:00
saopig1andClaude Sonnet 4.6 914180792d feat(web): AvatarUpload component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 20:11:43 +08:00
saopig1andClaude Sonnet 4.6 935656dc4f feat(api): /api/bot/:id/avatar GET/PUT/DELETE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 20:10:06 +08:00
saopig1andClaude Sonnet 4.6 2dd6a9e20d feat(bot): load custom avatar on instance startup
Thread AvatarStore from index.ts → BotManager → BotInstance so every
BotInstance reads the persisted custom avatar from disk at construction
time and hands it to BotProfileManager via setCustomAvatar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 20:07:47 +08:00
saopig1andClaude Sonnet 4.6 ffa27d7224 feat(profile): custom avatar with idle/playback precedence
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 20:05:00 +08:00
saopig1andClaude Sonnet 4.6 edd0fc58eb feat(data): avatar file store helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 20:00:56 +08:00
saopig1andClaude Sonnet 4.6 366edf7843 feat(db): custom_avatar_path column + accessors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 19:59:20 +08:00
saopig1andClaude Opus 4.7 ff5502be2f docs(plan): custom avatar + album search implementation plans
Two independent plans for spec 2026-05-07; each yields its own PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:55:52 +08:00
saopig1andClaude Opus 4.7 4407cd0c67 docs(spec): custom bot avatar + album search design
Spec for issue #51: custom avatar with idle/playback precedence rules,
and surfacing albums in search results + album detail playback. Two
features grouped in one spec; will land as two separate PRs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:49:53 +08:00
TIANYAO ZHANG b8c12e0291 Merge pull request #53 from L1uyQwQ/codex/mobile-responsive-controls
fix: improve mobile web responsive controls
2026-05-07 19:39:51 +08:00
TIANYAO ZHANG e7411ee7fb Merge pull request #55 from ZHANGTIANYAO1/fix/netease-ffmpeg-ua
fix(player): WinHTTP fallback for /jdymusic/ CDN that drops Node TCP
2026-05-07 19:37:58 +08:00
TIANYAO ZHANG ce88d12a60 Merge pull request #54 from ZHANGTIANYAO1/fix/jdymusic-powershell-fallback
fix(player): WinHTTP (PowerShell) fallback for /jdymusic/ CDN
v1.4.2
2026-05-07 19:18:02 +08:00
saopig1andClaude Opus 4.7 719ceae303 fix(player): WinHTTP fallback for /jdymusic/ CDN that drops Node TCP
The old jdymusic CDN (e.g. m701/m801.music.126.net/.../jdymusic/obj/...)
intermittently drops connections from Node's HTTP stack — even with a
browser UA — while the same URL fetched via WinHTTP works. Newer paths
(/jd-musicrep-ts/, /ymusic/) do not have this restriction.

On Windows, dispatch /jdymusic/ URLs through `System.Net.WebClient` in
PowerShell into a temp file, then run ffmpeg against the file. Other
URLs and non-Windows platforms keep the direct ffmpeg + browser-UA path
from the previous commit. Also fix buildFfmpegArgs to omit HTTP-only
flags (-reconnect_*, -headers) when the input is a local file path —
new ffmpeg builds reject these as "Option not found".

stop() now also kills any in-flight PowerShell downloader and removes
the temp dir, so cancellation is clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:08:58 +08:00
saopig1andClaude Opus 4.7 52ece9321f fix(player): WinHTTP fallback for /jdymusic/ CDN that drops Node TCP
The old jdymusic CDN (e.g. m701/m801.music.126.net/.../jdymusic/obj/...)
intermittently drops connections from Node's HTTP stack — even with a
browser UA — while the same URL fetched via WinHTTP works. Newer paths
(/jd-musicrep-ts/, /ymusic/) do not have this restriction.

On Windows, dispatch /jdymusic/ URLs through `System.Net.WebClient` in
PowerShell into a temp file, then run ffmpeg against the file. Other
URLs and non-Windows platforms keep the direct ffmpeg + browser-UA path
from the previous commit. Also fix buildFfmpegArgs to omit HTTP-only
flags (-reconnect_*, -headers) when the input is a local file path —
new ffmpeg builds reject these as "Option not found".

stop() now also kills any in-flight PowerShell downloader and removes
the temp dir, so cancellation is clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:08:00 +08:00
L1uyQwQ 0bf34d8652 fix mobile web responsive controls 2026-05-07 16:58:31 +08:00
TIANYAO ZHANG 9179780afb Merge pull request #52 from ZHANGTIANYAO1/fix/netease-ffmpeg-ua 2026-05-07 16:33:25 +08:00
saopig1andClaude Opus 4.7 f76500cfb2 fix(player): set browser UA + Referer for Netease CDN to stop auto-skip
Netease's m701/m801.music.126.net CDN RST connections from FFmpeg's
default Lavf UA. The bot's frame loop treats an early ffmpeg exit as
trackEnd and advances the queue, which surfaced as songs auto-skipping
mid-playlist. Add a browser UA + music.163.com Referer for these URLs,
extract args into a tested buildFfmpegArgs(), and harden reconnect
flags (delay_max 5 -> 30, plus reconnect_on_network_error /
reconnect_on_http_error). Verified by A/B running ffmpeg against the
same fresh CDN URL: legacy args got 0 bytes + "End of file" reading
HTTP response; fixed args streamed the full track to completion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:18:23 +08:00
saopig1andClaude Opus 4.7 7eb96d9068 ci: publish multi-arch Docker image to GHCR on tag push
Add GitHub Actions workflow that builds linux/amd64 + linux/arm64
images via buildx + QEMU and pushes to ghcr.io/zhangtianyao1/teamspeak-music-bot
on v*.*.* tag pushes (or manual dispatch). Switch docker-compose.yml
to pull the prebuilt image so NAS / non-build environments can deploy
without a local toolchain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.4.1
2026-05-06 17:21:37 +08:00
TIANYAO ZHANG 001fb5a451 Merge pull request #50 from ZHANGTIANYAO1/feature/design-implementation
Web UI redesign + per-platform tabs + play-next + audit fixes
v1.4.0
2026-05-06 17:09:59 +08:00
saopig1andClaude Opus 4.7 73f3f7749a fix: cmdPrev retry-skip + show SongCard actions on touch devices
Two corner-case fixes for the prev-history + play-next feature:

1. cmdPrev only tried queue.prev() once. instance.playNext's auto-
   advance retry-skip pushes failed songs into the same history stack,
   so a single prev frequently lands on an unplayable song — returning
   "Cannot play previous song" while leaving queue.currentIndex stuck
   mid-failure (causing next() to skip past the actually-playing song).
   Retry up to 4 times so prev finds a playable history entry, matching
   the retry budget already used by playNext for auto-advance.

2. SongCard.song-actions has opacity:0 by default and is revealed via
   parent :hover. Touch devices have no hover, so all three action
   buttons (Play / Play Next / Add) were invisible to phone/tablet
   users. Add @media (pointer: coarse) → opacity:1 to always show on
   touch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:06:18 +08:00
saopig1andClaude Opus 4.7 fab8c194e3 fix(player): play-next must use insertedAt, not size-1, when idle
Final review of caeef65 caught a stale-currentIndex bug in
/play-next-song and cmdPlayNext: when the player is idle but
queue.currentIndex >= 0 (natural end-of-track, or playNext gave
up after retries without queue.clear), addNext splices mid-queue
and playAt(size-1) jumps PAST the inserted song to whatever
was last. Capture the insertion slot before calling addNext and
promote that exact index instead.

Add a regression test covering the [a,b,c,d] queue with
currentIndex=1 case -- splice at 2 yields x at index 2, but
size-1 would point at d (index 4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:00:05 +08:00
saopig1andClaude Opus 4.7 caeef65cdb feat(web): wire @playNext on all SongCard call sites
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>
2026-05-06 16:43:10 +08:00
saopig1andClaude Opus 4.7 ad4fb5d3c7 feat(web): third 'play next' action on SongCard
Button sits between Play and Add to queue. Icon is mdi:playlist-play,
title '下一首播放'. Emits 'playNext' for callers to wire up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:41:47 +08:00
saopig1andClaude Opus 4.7 44b0b5c31c feat(web): playNextSong store action
Posts to /play-next-song, surfaces failures via the existing Toast,
and refreshes the queue panel so the inserted song appears.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:40:12 +08:00
saopig1andClaude Opus 4.7 3e795d9a83 feat: !playnext command and /play-next-song endpoint
Mirror of !play / /play-song but uses queue.addNext to splice in at
currentIndex+1 instead of clearing the queue. Idle bot still starts
the song immediately. Adds 'playnext' / 'pn' to AUDIO_COMMANDS, the
command switch, and the help text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:37:52 +08:00
saopig1andClaude Opus 4.7 11c6a3f51b feat(queue): addNext inserts a song to play right after current
Splices into currentIndex+1 and shifts both playedIndices and the
history back-stack to keep references valid. Falls through to plain
push when nothing is playing so the idle-bot "add → start playing"
flow is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:34:32 +08:00
saopig1andClaude Opus 4.7 30fd8a19b8 fix(queue): restore playedIndices clear in playAt; test HISTORY_LIMIT
Code review of 390d3fa flagged that dropping playedIndices.clear() from
playAt is an unnecessary behavior change. The two operations (push
history, reset random pool) are not in tension — restoring the clear
preserves the original 'explicit pick restarts shuffle' semantic that
Random mode users rely on, while still tracking the prev-history stack.

Also add a regression test that the 50-entry HISTORY_LIMIT cap works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:32:23 +08:00
saopig1andClaude Opus 4.7 390d3fa782 feat(queue): history-aware prev that walks real play history
In random modes, prev was just doing currentIndex-1 in the array, which
has no relationship to what the user actually played before. Add a
50-entry back-stack that's pushed by next/playAt, popped by prev, reset
on play/clear/setMode, and shifted by remove. Sequential and Loop modes
keep their old fallback for the case where history is empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:28:52 +08:00
saopig1andClaude Opus 4.7 c4b7cfaa2f refactor(queue): add history field and pushHistory helper
Inert in this commit — no callers yet. Sets up the back-stack used
by the upcoming history-aware prev rewrite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:24:52 +08:00
saopig1andClaude Opus 4.7 94c60ad465 docs: implementation plan for history-aware prev + play next
Eight tasks: queue history field/helper, history-aware prev with
TDD tests, addNext with TDD tests, REST + command surface, store
action, SongCard third button, view wiring, final verify + smoke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:10:57 +08:00
saopig1andClaude Opus 4.7 58bdfb94a9 docs: spec for history-aware prev + play-next insert
Two queue features:
- prev walks back through actual play history (50-entry stack), so
  random modes navigate predictably instead of falling back to the
  meaningless currentIndex-1 array walk.
- addNext inserts at currentIndex+1, exposed via new /play-next-song
  endpoint, !playnext command, and a third "下一首播放" button on
  SongCard. Insert path keeps playedIndices and history index refs
  valid by shifting entries > currentIndex.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:06:56 +08:00
saopig1andClaude Opus 4.7 59fb3ee3bd fix: address all known follow-up issues except NetEase batch precheck
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>
2026-05-06 16:00:48 +08:00
saopig1andClaude Opus 4.7 221079b89c fix: more corner-case audit — partial-failure UX + defensive parsing
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>
2026-05-06 15:52:49 +08:00
saopig1andClaude Opus 4.7 892d9f7959 fix: corner-case audit — distinguish failures, recover from blips
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>
2026-05-06 15:46:31 +08:00
saopig1andClaude Opus 4.7 3d0aca52d0 fix(qq): batch-precheck playable URLs before queueing playlist
Many users' QQ playlists (especially collected/subscribed ones) contain
a large fraction of songs that return result=104003 (region/copyright
restricted) for the current account. Sequential retry-skip wasted time
guessing — for the user's ACG古风 collected playlist, only 3 of 115
songs are actually streamable, so a 20-retry budget had < 50% chance
of finding a hit before giving up.

Add a getPlayableSongIds(ids) method to the QQ provider that calls the
upstream /getMusicPlay with a comma-separated batch of mids — the
wrapper resolves all of them in a single upstream call (~2-3s for 100+
songs). /play-playlist now duck-types this method and, when present,
filters the playlist down to playable songs before queueing.

Response message reports "Loaded N of M songs (rest are copyright/
region restricted)" so the user sees exactly what's happening instead
of guessing why the queue is short or playback didn't start.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:34:16 +08:00
saopig1andClaude Opus 4.7 4301da2e37 fix(player): bigger retry budget + return value for /play-playlist
The previous fix for "播放全部 没有反应" hooked into playNext but had
two problems:

1. Used !!queue.current() to detect success, which is always true
   after queue.next() advanced — so the response message would lie
   about playback starting even when all retries failed.

2. The default 3-retry budget is sized for "next song couldn't
   resolve, skip it" cases. User-initiated playlist plays commonly
   hit long contiguous runs of QQ 104003 songs in collected playlists
   (entire ACG/古风 packs can be uniformly unstreamable in some
   regions), so 3-4 attempts wasn't enough.

Make playNext return whether a song actually started, and accept a
maxRetries parameter. /play-playlist now uses 20 retries — high
enough to clear typical unplayable runs, bounded enough that fully
unstreamable playlists still surface a clear "none were playable"
message rather than hanging.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:25:59 +08:00
saopig1andClaude Opus 4.7 82655afa20 fix(player): retry-skip in /play-playlist when first song can't resolve
QQ Music sometimes refuses to issue a play URL for individual songs
(upstream returns result=104003, "no copyright/payment"). The
/play-playlist endpoint only called resolveAndPlay once on the first
song, so when that one was 104003 the bot would sit silently with no
feedback — exactly what looked like "播放全部 没有反应".

Make BotInstance.playNext() public and call it as a fallback when the
first resolveAndPlay fails. playNext already has the 3-attempt
retry-skip used by trackEnd auto-advance, so we get the same
graceful skip behavior for explicit playlist plays.

Also tighten the response message so the user can tell when the bot
loaded songs but none were playable.

Single-song /play-song still returns "Cannot play" on failure (no
queue to advance through); UX surfacing of that goes in a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:43:01 +08:00
saopig1andClaude Opus 4.7 5799891e4f feat(web): bot profile toggles in Settings
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>
2026-05-06 14:15:56 +08:00
saopig1andClaude Opus 4.7 900191eca5 fix(api): add getPlaylistDetail provider method for QQ playlist detail
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>
2026-05-06 14:05:48 +08:00
saopig1andClaude Opus 4.7 e8d2f1ad98 fix(qq): correct user-playlists field mapping; add collected playlists and daily recommend
Three fixes to the QQ Music provider, all surfaced by enabling the QQ
tab on Home/Library:

1. getUserPlaylists field names were wrong. The upstream
   fcg_get_profile_homepage actually returns title/picurl/subtitle,
   not dissname/imgurl/song_count, so all playlist cards rendered as
   empty placeholders. Map title->name, picurl->coverUrl, and parse
   '(\d+)首' from subtitle for songCount.

2. getUserPlaylists only returned playlists the user CREATED. Now also
   fetches COLLECTED playlists from c.y.qq.com fcg_get_profile_order_asset
   (reqtype=3, requires g_tk derived from p_skey cookie) and concatenates
   them after the created list — same order as the QQ desktop app.

3. Add getDailyRecommendSongs implementation backed by /getNewSongs
   (新歌速递, ~20 newest songs). The QQ provider previously didn't
   implement this method, so the daily-recommend QQ tab was empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:56:54 +08:00
saopig1andClaude Opus 4.7 ec02887d22 fix(web): post-review fixes for source tabs
- 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>
2026-05-06 13:26:46 +08:00
saopig1andClaude Opus 4.7 ac55a346be feat(web): per-platform source tabs on Home and Library
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>
2026-05-06 13:19:49 +08:00
saopig1andClaude Opus 4.7 a216709227 feat(web): add SourceTabs component for platform switcher
Presentational component for switching between netease and qq music
sources. Auto-hides when fewer than 2 sources are passed in. Mobile
breakpoint enlarges touch target to 36px.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:12:55 +08:00
saopig1andClaude Opus 4.7 4a990bfde3 docs: implementation plan for music source tabs
Step-by-step plan for per-platform tabs on Home and Library: SourceTabs
component, store refactor with authStatus and per-platform data, view
migrations with localStorage persistence, build verification, and
3-scenario manual smoke test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:08:38 +08:00