Per https://github.com/ZHANGTIANYAO1/teamspeak-music-bot/issues/61:
- Remove searchid param (its presence now causes empty results)
- Enforce num_per_page >= 10 (lower values return empty)
- Fix search_type: 2 for albums, 3 for playlists (8 was "user")
Now uses the fixed musicu.fcg as primary (supports songs + albums +
playlists), with client_search_cp as fallback.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
u.y.qq.com/cgi-bin/musicu.fcg started returning is_filter=-9 (empty results)
for unauthenticated requests. Primary search now uses the classic
c.y.qq.com/soso/fcgi-bin/client_search_cp endpoint, with the old musicu.fcg
path kept as a fallback in case the primary endpoint changes format or goes
down.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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 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>
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>
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>
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>
Repoints the `@sansenjian/qq-music-api` dependency from the public npm
release to a local fork at ../qq-music-api, which ships a corrected
getMusicPlay that:
- drops the hardcoded-sign GET path (no longer honored by QQ's vkey
server for VIP entitlement lookups)
- POSTs JSON directly to u.y.qq.com/cgi-bin/musicu.fcg (mirroring
the library's own getLyric.ts pattern)
- extracts qqmusic_key from the forwarded cookie and passes it as
`comm.authst` — the inline auth field the jsososo/QQMusicApi
reference implementation sets
- uses `ct: 19` (was 24) to match the community reference
For accounts that actually have entitlement to a given track, this
now returns the real VIP URL. For accounts that don't, QQ's vkey
server still returns result=104003 with empty purl — this is correct
server-side behavior and not a bug. Verified by observing the real
QQ Music web player on y.qq.com fall back to the same 30-second
preview on a logged-in account that lacks the specific track tier.
Supporting changes:
src/music/api-server.ts
The fork (v2.2.11) stopped auto-starting a Koa server on import —
it only listens when run as `require.main`. Explicitly import the
default Koa app and call .listen() with a server handle we can
clean up on shutdown. Without this fix, port 3200 silently fails
to bind and every QQ endpoint 502s.
src/music/qq.ts (getSongDetail)
The library's /getSongInfo endpoint returns upstream code 500001
because its param format no longer matches QQ's current API.
resolveAndPlay only needs `id` + `platform` to fetch a play URL,
so fall through to a minimal stub on /getSongInfo failure. This
unblocks /play-by-id and /add-by-id for QQ — they had been
returning "Song not found" for every QQ track regardless of
entitlement.
scripts/qq_browser_login.py
Visible-browser diagnostic tool that opens Chromium at y.qq.com,
auto-detects login via uin cookie poll, captures the full
post-login cookie set, tests it against /getMusicPlay for 稻香,
and writes the cookie to data/cookies/qq.json only if VIP
actually unlocks. On failure, dumps the full cookie to
data/cookies/qq.browser-capture.json for OAuth-vs-browser diff.
scripts/qq_verify_entitlement.py
Companion diagnostic: opens the real QQ Music web player at a
specific song's detail page so the user can manually click play
and verify whether their account has entitlement — independent
of any code path in this project. If the browser plays the full
song, HTTP 104003 is a request-signing issue; if the browser
also falls back to a 30-second preview, the account lacks the
tier/album purchase and no code fix can change that.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
QR login against QQ Music has been silently broken: every call to
checkQrCodeStatus returned "expired", so the scan-and-confirm cycle
never completed even when the user successfully scanned the code. The
root cause was four independent bugs in our wrapper talking past the
library's actual HTTP shape.
1. getQrCode lost ptqrtoken.
/getQQLoginQr returns { img, qrsig, ptqrtoken }, but we stored only
one of them in the single `key` field (picking qrsig, falling back
to ptqrtoken). The polling endpoint needs BOTH — passing only one
fails with 400 "参数错误". Fix: pack both into the opaque `key` as
"qrsig|ptqrtoken" and split on the receive side.
2. checkQrCodeStatus used GET.
@sansenjian/qq-music-api 2.x registers /checkQQLoginQr as POST only
(router.js: `router.post('/checkQQLoginQr', ...)`). GET returns 405
Method Not Allowed, axios throws, the catch returns "expired".
Fix: api.post(url, null, { params }).
3. checkQrCodeStatus parsed the wrong response shape.
The endpoint uses customResponse, not successResponse, so axios sees
the body directly (no { response: ... } wrapper). The actual shape
for each state is:
waiting: { isOk: false, refresh: false, message: '未扫描二维码' }
expired: { isOk: false, refresh: true, message: '二维码已失效' }
success: { isOk: true, message: '登录成功', session: { cookie } }
We were looking for a numeric `code === 0/1/2` field that does not
exist, so every state fell through to "expired". Fix: switch on
isOk / refresh / message.
4. Cookie read from the wrong path on success.
On isOk=true the cookie lives at res.data.session.cookie, not
res.data.cookie — so even if everything else had worked, the cookie
would never have been saved. Fix: read session.cookie.
Also rewrites getAuthStatus to actually validate the cookie:
5. getAuthStatus hit a non-validating endpoint.
/getUserAvatar is not registered on the library's main router; the
real route is /user/getUserAvatar, and even that just builds a
static avatar URL from a uin without round-tripping through QQ
Music with the cookie. The result: the bot happily persisted any
user-supplied cookie to disk and sent it on every request while
every downstream login check returned "not logged in". Fix: parse
uin from the cookie, call /user/getUserPlaylists (which actually
hits QQ Music with the cookie), and derive the avatar URL from the
uin via q.qlogo.cn/headimg_dl.
This is the same getAuthStatus fix that was sitting on the
claude/bot-shutdown-disconnect-cwFvF branch, now combined with the
QR login repairs.
Verification:
- tsc --noEmit clean
- vitest: 93/93 pass
- Live: POST /api/auth/qrcode platform=qq returns both tokens packed
into `key`; polling a freshly-issued QR returns {"status":"waiting"}
instead of the old {"status":"expired"}; raw library response is
{"isOk":false,"refresh":false,"message":"未扫描二维码"} as expected.
- Regression: netease and bilibili QR flows still produce non-empty
qrUrl/key — no collateral damage to the other providers.
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>