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
+46 -22
+46 -22
View File
@@ -153,29 +153,41 @@ export class QQMusicProvider implements MusicProvider {
async getQrCode(): Promise<QrCodeResult> { async getQrCode(): Promise<QrCodeResult> {
const res = await this.api.get("/getQQLoginQr"); 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 { return {
qrUrl: "", qrUrl: "",
qrImg: res.data?.img ?? "", qrImg: res.data?.img ?? "",
key: res.data?.qrsig ?? res.data?.ptqrtoken ?? "", key: qrsig && ptqrtoken ? `${qrsig}|${ptqrtoken}` : "",
}; };
} }
async checkQrCodeStatus( async checkQrCodeStatus(
key: string key: string
): Promise<"waiting" | "scanned" | "confirmed" | "expired"> { ): Promise<"waiting" | "scanned" | "confirmed" | "expired"> {
const res = await this.api.get("/checkQQLoginQr", { const [qrsig, ptqrtoken] = key.split("|");
params: { qrsig: key }, if (!qrsig || !ptqrtoken) return "expired";
}); try {
const code = res.data?.code ?? res.data?.response?.code; // /checkQQLoginQr is POST, and the success body is
if (code === 0) { // { isOk, refresh, message, session: { cookie, ... } }
if (res.data?.cookie) { const res = await this.api.post("/checkQQLoginQr", null, {
this.cookie = res.data.cookie; params: { qrsig, ptqrtoken },
});
if (res.data?.isOk) {
const cookie = res.data?.session?.cookie;
if (cookie) {
this.cookie = cookie;
}
return "confirmed";
} }
return "confirmed"; if (res.data?.refresh) return "expired";
// The upstream lib does not distinguish "scanned" from "waiting"
return "waiting";
} catch {
return "expired";
} }
if (code === 1) return "scanned";
if (code === 2) return "waiting";
return "expired";
} }
setCookie(cookie: string): void { setCookie(cookie: string): void {
@@ -188,20 +200,32 @@ export class QQMusicProvider implements MusicProvider {
async getAuthStatus(): Promise<AuthStatus> { async getAuthStatus(): Promise<AuthStatus> {
if (!this.cookie) return { loggedIn: false }; 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 { try {
const res = await this.api.get("/getUserAvatar", { const res = await this.api.get("/user/getUserPlaylists", {
params: { ...this.cookieParams }, params: { uin, limit: 1, ...this.cookieParams },
}); });
if (res.data?.response?.data) { if (res.data?.response?.code !== 0) {
return { return { loggedIn: false };
loggedIn: true,
nickname: res.data.response.data.nickname,
avatarUrl: res.data.response.data.headpic,
};
} }
return {
loggedIn: true,
nickname: `QQ ${uin}`,
avatarUrl: `https://q.qlogo.cn/headimg_dl?dst_uin=${uin}&spec=140`,
};
} catch { } catch {
// ignore return { loggedIn: false };
} }
return { loggedIn: false };
} }
} }