Fix QQ Music cookie auth flow against @sansenjian/qq-music-api 2.x

The previous implementation pointed at endpoints and response shapes that
don't match the current @sansenjian/qq-music-api. As a result the bot was
happily saving a user-supplied cookie to disk and sending it on every
request, but every downstream login check returned "not logged in".

- getAuthStatus:
    /getUserAvatar is not a registered route (the real one is
    /user/getUserAvatar), and even then that handler only builds a static
    avatar URL from a uin and never talks to QQ, so it can't validate a
    cookie. Parse uin from the cookie and round-trip it through
    /user/getUserPlaylists, which actually hits QQ Music with the cookie,
    then derive the avatar URL from the uin.

- getQrCode:
    checkQQLoginQr requires BOTH qrsig and ptqrtoken, but only qrsig was
    being returned as the poll key. Encode both into the opaque key.

- checkQrCodeStatus:
    /checkQQLoginQr is POST, not GET, and the response is
    { isOk, refresh, session: { cookie, ... } } rather than { code, cookie }.
    Switch to POST, pass both params, and read session.cookie on success.

https://claude.ai/code/session_01LH83VXwxPY8f9RQ5HgfPby
This commit is contained in:
Claude committed 2026-04-10 17:39:42 +00:00
1 parent a8eb1bebd9
commit 79c50c6f29
1 file changed
+41 -17
+41 -17
View File
@@ -153,30 +153,42 @@ export class QQMusicProvider implements MusicProvider {
async getQrCode(): Promise<QrCodeResult> {
const res = await this.api.get("/getQQLoginQr");
const qrsig = res.data?.qrsig ?? "";
const ptqrtoken = res.data?.ptqrtoken ?? "";
// @sansenjian/qq-music-api's checkQQLoginQr requires BOTH qrsig and
// ptqrtoken, so encode both into the opaque key the caller polls with.
return {
qrUrl: "",
qrImg: res.data?.img ?? "",
key: res.data?.qrsig ?? res.data?.ptqrtoken ?? "",
key: qrsig && ptqrtoken ? `${qrsig}|${ptqrtoken}` : "",
};
}
async checkQrCodeStatus(
key: string
): Promise<"waiting" | "scanned" | "confirmed" | "expired"> {
const res = await this.api.get("/checkQQLoginQr", {
params: { qrsig: key },
const [qrsig, ptqrtoken] = key.split("|");
if (!qrsig || !ptqrtoken) return "expired";
try {
// /checkQQLoginQr is POST, and the success body is
// { isOk, refresh, message, session: { cookie, ... } }
const res = await this.api.post("/checkQQLoginQr", null, {
params: { qrsig, ptqrtoken },
});
const code = res.data?.code ?? res.data?.response?.code;
if (code === 0) {
if (res.data?.cookie) {
this.cookie = res.data.cookie;
if (res.data?.isOk) {
const cookie = res.data?.session?.cookie;
if (cookie) {
this.cookie = cookie;
}
return "confirmed";
}
if (code === 1) return "scanned";
if (code === 2) return "waiting";
if (res.data?.refresh) return "expired";
// The upstream lib does not distinguish "scanned" from "waiting"
return "waiting";
} catch {
return "expired";
}
}
setCookie(cookie: string): void {
this.cookie = cookie;
@@ -188,20 +200,32 @@ export class QQMusicProvider implements MusicProvider {
async getAuthStatus(): Promise<AuthStatus> {
if (!this.cookie) return { loggedIn: false };
// @sansenjian/qq-music-api does not expose a dedicated "am I logged in"
// endpoint. /user/getUserAvatar only builds a static avatar URL from a
// uin and never talks to QQ, so it cannot be used to validate a cookie.
// Instead, parse the uin from the cookie and round-trip it through
// /user/getUserPlaylists, which actually hits QQ Music's servers using
// the provided cookie. A successful response (code=0) means the cookie
// is still valid.
const uinMatch = this.cookie.match(/(?:^|;\s*)uin=([^;]+)/);
const uin = uinMatch?.[1];
if (!uin) return { loggedIn: false };
try {
const res = await this.api.get("/getUserAvatar", {
params: { ...this.cookieParams },
const res = await this.api.get("/user/getUserPlaylists", {
params: { uin, limit: 1, ...this.cookieParams },
});
if (res.data?.response?.data) {
if (res.data?.response?.code !== 0) {
return { loggedIn: false };
}
return {
loggedIn: true,
nickname: res.data.response.data.nickname,
avatarUrl: res.data.response.data.headpic,
nickname: `QQ ${uin}`,
avatarUrl: `https://q.qlogo.cn/headimg_dl?dst_uin=${uin}&spec=140`,
};
}
} catch {
// ignore
}
return { loggedIn: false };
}
}
}