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>