qqmusic-api-enhanced 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/interface.d.ts CHANGED
@@ -34,8 +34,11 @@ export interface LoginQrCreateResult {
34
34
  qrsig?: string;
35
35
  ptqrtoken?: string | number;
36
36
  uuid?: string;
37
- qrimg: string;
38
- url: string;
37
+ qrcode?: string;
38
+ qrcodeID?: string;
39
+ qrurl?: string;
40
+ qrimg?: string;
41
+ url?: string;
39
42
  }
40
43
 
41
44
  export interface LoginQrCheckQuery extends RequestBaseConfig {
@@ -43,8 +46,14 @@ export interface LoginQrCheckQuery extends RequestBaseConfig {
43
46
  type?: "qq" | "wx" | "mobile";
44
47
  /** 手机 QQ 轮询凭证,来源于 login_qr_create */
45
48
  qrsig?: string;
49
+ /** 手机 QQ 轮询 token,来源于 login_qr_create */
50
+ ptqrtoken?: string | number;
46
51
  /** 微信轮询凭证 uuid,来源于 login_qr_create */
47
52
  uuid?: string;
53
+ /** QQ 音乐移动端 App 扫码 qrcodeID,来源于 login_qr_create */
54
+ qrcodeID?: string;
55
+ /** 兼容通用 key 参数 */
56
+ key?: string;
48
57
  }
49
58
 
50
59
  export interface LoginQrCheckResult {
@@ -54,6 +63,26 @@ export interface LoginQrCheckResult {
54
63
  musickey?: string;
55
64
  cookie?: Record<string, string>;
56
65
  cookieStr?: string;
66
+ data?: Record<string, any>;
67
+ }
68
+
69
+ export interface NormalizedVipInfo {
70
+ vipStatus: "vip" | "nonVip" | "unknown";
71
+ vipTier?: "svip" | "vip";
72
+ vipLevel?: number;
73
+ vipYear?: boolean;
74
+ vipExpireTime?: number;
75
+ }
76
+
77
+ export interface NormalizedLoginAccount {
78
+ userId: string;
79
+ nickname: string;
80
+ avatarUrl?: string;
81
+ vipStatus: "vip" | "nonVip" | "unknown";
82
+ vipTier?: "svip" | "vip";
83
+ vipLevel?: number;
84
+ vipYear?: boolean;
85
+ vipExpireTime?: number;
57
86
  }
58
87
 
59
88
  export interface LoginStatusQuery extends RequestBaseConfig {}
@@ -61,18 +90,37 @@ export interface LoginStatusQuery extends RequestBaseConfig {}
61
90
  export interface LoginStatusResult {
62
91
  code: number;
63
92
  isLogin: boolean;
64
- uin?: string;
93
+ uin: string;
94
+ nickname: string;
95
+ avatarUrl?: string;
96
+ vip: NormalizedVipInfo;
97
+ account: NormalizedLoginAccount;
98
+ data?: Record<string, any>;
65
99
  userInfo?: Record<string, any>;
66
100
  }
67
101
 
68
- export interface LoginRefreshQuery extends RequestBaseConfig {}
102
+ export interface LoginRefreshQuery extends RequestBaseConfig {
103
+ openid?: string;
104
+ refresh_token?: string;
105
+ refresh_key?: string;
106
+ }
69
107
 
70
108
  export interface LoginRefreshResult {
71
109
  code: number;
72
110
  musickey?: string;
111
+ cookieStr?: string;
112
+ data?: Record<string, any>;
73
113
  raw?: Record<string, any>;
74
114
  }
75
115
 
116
+ export interface UserVipInfoQuery extends RequestBaseConfig {}
117
+
118
+ export interface UserVipInfoResult {
119
+ code: number;
120
+ data: Record<string, any>;
121
+ vip: NormalizedVipInfo;
122
+ }
123
+
76
124
  // ==============================
77
125
  // 2. 歌曲与播放 (Songs & Playback)
78
126
  // ==============================
@@ -281,6 +329,8 @@ export function song_detail(query?: SongDetailQuery): Promise<Response<SongDetai
281
329
  export function song_url(query?: SongUrlQuery): Promise<Response<SongUrlResult>>;
282
330
  export function user_detail(query?: UserDetailQuery): Promise<Response<UserDetailResult>>;
283
331
  export function user_playlist(query?: UserPlaylistQuery): Promise<Response<UserPlaylistResult>>;
332
+ export function user_vip_info(query?: UserVipInfoQuery): Promise<Response<UserVipInfoResult>>;
333
+ export function deriveVipInfo(body: any): NormalizedVipInfo;
284
334
 
285
335
  // 基础网络调度器与工具函数
286
336
  export function request(arg1: string, arg2?: any, arg3?: any, arg4?: any): Promise<ApiResponse>;
@@ -312,6 +362,8 @@ declare const api: {
312
362
  song_url: typeof song_url;
313
363
  user_detail: typeof user_detail;
314
364
  user_playlist: typeof user_playlist;
365
+ user_vip_info: typeof user_vip_info;
366
+ deriveVipInfo: typeof deriveVipInfo;
315
367
  request: typeof request;
316
368
  requestCgi: typeof requestCgi;
317
369
  requestRaw: typeof requestRaw;
@@ -327,3 +379,4 @@ declare const api: {
327
379
  };
328
380
 
329
381
  export default api;
382
+
package/main.js CHANGED
@@ -13,6 +13,7 @@ import song_detail_fn from "./module/song_detail.js";
13
13
  import song_url_fn from "./module/song_url.js";
14
14
  import user_detail_fn from "./module/user_detail.js";
15
15
  import user_playlist_fn from "./module/user_playlist.js";
16
+ import user_vip_info_fn, { deriveVipInfo, } from "./module/user_vip_info.js";
16
17
  import { cookieToJson, cookieToString, parseSetCookie } from "./util/cookie.js";
17
18
  import { getGtk, getGuid, getSearchId, hash33, qrcDecrypt } from "./util/crypto.js";
18
19
  import { request, requestCgi, requestRaw } from "./util/request.js";
@@ -32,7 +33,8 @@ export const song_detail = (query = {}) => song_detail_fn(query, request);
32
33
  export const song_url = (query = {}) => song_url_fn(query, request);
33
34
  export const user_detail = (query = {}) => user_detail_fn(query, request);
34
35
  export const user_playlist = (query = {}) => user_playlist_fn(query, request);
35
- export { request, requestCgi, requestRaw, zzcSign, qrcDecrypt, hash33, getGtk, getGuid, getSearchId, cookieToJson, cookieToString, parseSetCookie, };
36
+ export const user_vip_info = (query = {}) => user_vip_info_fn(query, request);
37
+ export { request, requestCgi, requestRaw, zzcSign, qrcDecrypt, hash33, getGtk, getGuid, getSearchId, cookieToJson, cookieToString, parseSetCookie, deriveVipInfo, };
36
38
  const api = {
37
39
  artist_detail,
38
40
  artist_songs,
@@ -49,6 +51,8 @@ const api = {
49
51
  song_url,
50
52
  user_detail,
51
53
  user_playlist,
54
+ user_vip_info,
55
+ deriveVipInfo,
52
56
  request,
53
57
  requestCgi,
54
58
  requestRaw,
package/main.ts CHANGED
@@ -3,7 +3,10 @@ import artist_songs_fn from "./module/artist_songs.js";
3
3
  import login_qr_check_fn from "./module/login_qr_check.js";
4
4
  import login_qr_create_fn from "./module/login_qr_create.js";
5
5
  import login_refresh_fn from "./module/login_refresh.js";
6
- import login_status_fn from "./module/login_status.js";
6
+ import login_status_fn, {
7
+ type LoginStatusResult,
8
+ type NormalizedLoginAccount,
9
+ } from "./module/login_status.js";
7
10
  import lyric_fn from "./module/lyric.js";
8
11
  import playlist_detail_fn from "./module/playlist_detail.js";
9
12
  import playlist_track_all_fn from "./module/playlist_track_all.js";
@@ -13,6 +16,10 @@ import song_detail_fn from "./module/song_detail.js";
13
16
  import song_url_fn from "./module/song_url.js";
14
17
  import user_detail_fn from "./module/user_detail.js";
15
18
  import user_playlist_fn from "./module/user_playlist.js";
19
+ import user_vip_info_fn, {
20
+ deriveVipInfo,
21
+ type NormalizedVipInfo,
22
+ } from "./module/user_vip_info.js";
16
23
 
17
24
  import { cookieToJson, cookieToString, parseSetCookie } from "./util/cookie.js";
18
25
  import { getGtk, getGuid, getSearchId, hash33, qrcDecrypt } from "./util/crypto.js";
@@ -64,6 +71,9 @@ export const user_detail = (query: Parameters<typeof user_detail_fn>[0] = {}) =>
64
71
  export const user_playlist = (query: Parameters<typeof user_playlist_fn>[0] = {}) =>
65
72
  user_playlist_fn(query, request);
66
73
 
74
+ export const user_vip_info = (query: Parameters<typeof user_vip_info_fn>[0] = {}) =>
75
+ user_vip_info_fn(query, request);
76
+
67
77
  export {
68
78
  request,
69
79
  requestCgi,
@@ -77,7 +87,11 @@ export {
77
87
  cookieToJson,
78
88
  cookieToString,
79
89
  parseSetCookie,
90
+ deriveVipInfo,
80
91
  type ApiResponse,
92
+ type NormalizedVipInfo,
93
+ type NormalizedLoginAccount,
94
+ type LoginStatusResult,
81
95
  };
82
96
 
83
97
  const api = {
@@ -96,6 +110,8 @@ const api = {
96
110
  song_url,
97
111
  user_detail,
98
112
  user_playlist,
113
+ user_vip_info,
114
+ deriveVipInfo,
99
115
  request,
100
116
  requestCgi,
101
117
  requestRaw,
@@ -2,14 +2,17 @@ import { request, type ApiResponse } from "../util/request.js";
2
2
  /**
3
3
  * 轮询检查二维码登录状态.
4
4
  *
5
- * @param query.type 登录类型: 'qq' | 'wx'
5
+ * @param query.type 登录类型: 'qq' | 'wx' | 'mobile'
6
6
  * @param query.qrsig QQ 二维码凭证标识 (type=qq 时必填)
7
7
  * @param query.ptqrtoken QQ 二维码校验 token (type=qq 时必填)
8
8
  * @param query.uuid 微信登录会话 uuid (type=wx 时必填)
9
+ * @param query.qrcodeID QQ 音乐 App 扫码 qrcodeID (type=mobile 时必填)
9
10
  */
10
11
  export default function login_qr_check(query?: {
11
- type?: "qq" | "wx";
12
+ type?: "qq" | "wx" | "mobile";
12
13
  qrsig?: string;
13
14
  ptqrtoken?: string | number;
14
15
  uuid?: string;
16
+ qrcodeID?: string;
17
+ key?: string;
15
18
  }, req?: typeof request): Promise<ApiResponse>;
@@ -4,10 +4,11 @@ import { cookieToString, parseSetCookie } from "../util/cookie.js";
4
4
  /**
5
5
  * 轮询检查二维码登录状态.
6
6
  *
7
- * @param query.type 登录类型: 'qq' | 'wx'
7
+ * @param query.type 登录类型: 'qq' | 'wx' | 'mobile'
8
8
  * @param query.qrsig QQ 二维码凭证标识 (type=qq 时必填)
9
9
  * @param query.ptqrtoken QQ 二维码校验 token (type=qq 时必填)
10
10
  * @param query.uuid 微信登录会话 uuid (type=wx 时必填)
11
+ * @param query.qrcodeID QQ 音乐 App 扫码 qrcodeID (type=mobile 时必填)
11
12
  */
12
13
  export default async function login_qr_check(query = {}, req = request) {
13
14
  const type = query.type || "qq";
@@ -138,6 +139,76 @@ export default async function login_qr_check(query = {}, req = request) {
138
139
  cookie: resultCookie,
139
140
  };
140
141
  }
142
+ if (type === "mobile") {
143
+ const qrcodeID = query.qrcodeID || query.key;
144
+ if (!qrcodeID) {
145
+ return {
146
+ status: 400,
147
+ body: { code: 400, message: "缺少必要参数 qrcodeID" },
148
+ };
149
+ }
150
+ const cgiRes = await requestCgi("music.login.LoginServer", "CheckQRCode", {
151
+ qrcodeID,
152
+ tmeAppID: "qqmusic",
153
+ }, {
154
+ platform: "android",
155
+ });
156
+ const cgiBody = (cgiRes.body || {});
157
+ const code = cgiBody.code;
158
+ if (code === 40000) {
159
+ return {
160
+ status: 200,
161
+ body: { code: 801, message: "等待 QQ 音乐 App 扫码" },
162
+ };
163
+ }
164
+ if (code === 40001) {
165
+ return {
166
+ status: 200,
167
+ body: { code: 802, message: "已扫码,等待确认" },
168
+ };
169
+ }
170
+ if (code === 40002) {
171
+ return {
172
+ status: 200,
173
+ body: { code: 800, message: "二维码已过期,请刷新" },
174
+ };
175
+ }
176
+ if (code === 0) {
177
+ const cookies = cgiRes.cookie || [];
178
+ const cookieObj = parseSetCookie(cookies);
179
+ if (cgiBody.musicid && !cookieObj.uin)
180
+ cookieObj.uin = String(cgiBody.musicid);
181
+ if (cgiBody.musickey && !cookieObj.qm_keyst)
182
+ cookieObj.qm_keyst = String(cgiBody.musickey);
183
+ if (cgiBody.musickey && !cookieObj.qqmusic_key)
184
+ cookieObj.qqmusic_key = String(cgiBody.musickey);
185
+ if (cgiBody.openid && !cookieObj.openid)
186
+ cookieObj.openid = String(cgiBody.openid);
187
+ if (cgiBody.refresh_token && !cookieObj.refresh_token)
188
+ cookieObj.refresh_token = String(cgiBody.refresh_token);
189
+ const cookieStr = cookieToString(cookieObj);
190
+ return {
191
+ status: 200,
192
+ body: {
193
+ code: 200,
194
+ message: "登录成功",
195
+ uin: cookieObj.uin || String(cgiBody.musicid || ""),
196
+ musickey: cookieObj.qm_keyst || String(cgiBody.musickey || ""),
197
+ cookieStr,
198
+ data: cgiBody,
199
+ },
200
+ cookie: cookies,
201
+ };
202
+ }
203
+ return {
204
+ status: 200,
205
+ body: {
206
+ code: 801,
207
+ message: cgiBody.msg || "等待扫码",
208
+ data: cgiBody,
209
+ },
210
+ };
211
+ }
141
212
  // 微信扫码轮询
142
213
  const { uuid } = query;
143
214
  if (!uuid) {
@@ -5,17 +5,20 @@ import { cookieToString, parseSetCookie } from "../util/cookie.js";
5
5
  /**
6
6
  * 轮询检查二维码登录状态.
7
7
  *
8
- * @param query.type 登录类型: 'qq' | 'wx'
8
+ * @param query.type 登录类型: 'qq' | 'wx' | 'mobile'
9
9
  * @param query.qrsig QQ 二维码凭证标识 (type=qq 时必填)
10
10
  * @param query.ptqrtoken QQ 二维码校验 token (type=qq 时必填)
11
11
  * @param query.uuid 微信登录会话 uuid (type=wx 时必填)
12
+ * @param query.qrcodeID QQ 音乐 App 扫码 qrcodeID (type=mobile 时必填)
12
13
  */
13
14
  export default async function login_qr_check(
14
15
  query: {
15
- type?: "qq" | "wx";
16
+ type?: "qq" | "wx" | "mobile";
16
17
  qrsig?: string;
17
18
  ptqrtoken?: string | number;
18
19
  uuid?: string;
20
+ qrcodeID?: string;
21
+ key?: string;
19
22
  } = {},
20
23
  req = request
21
24
  ): Promise<ApiResponse> {
@@ -179,6 +182,82 @@ export default async function login_qr_check(
179
182
  };
180
183
  }
181
184
 
185
+ if (type === "mobile") {
186
+ const qrcodeID = query.qrcodeID || query.key;
187
+ if (!qrcodeID) {
188
+ return {
189
+ status: 400,
190
+ body: { code: 400, message: "缺少必要参数 qrcodeID" },
191
+ };
192
+ }
193
+
194
+ const cgiRes = await requestCgi(
195
+ "music.login.LoginServer",
196
+ "CheckQRCode",
197
+ {
198
+ qrcodeID,
199
+ tmeAppID: "qqmusic",
200
+ },
201
+ {
202
+ platform: "android",
203
+ }
204
+ );
205
+
206
+ const cgiBody = (cgiRes.body || {}) as any;
207
+ const code = cgiBody.code;
208
+
209
+ if (code === 40000) {
210
+ return {
211
+ status: 200,
212
+ body: { code: 801, message: "等待 QQ 音乐 App 扫码" },
213
+ };
214
+ }
215
+ if (code === 40001) {
216
+ return {
217
+ status: 200,
218
+ body: { code: 802, message: "已扫码,等待确认" },
219
+ };
220
+ }
221
+ if (code === 40002) {
222
+ return {
223
+ status: 200,
224
+ body: { code: 800, message: "二维码已过期,请刷新" },
225
+ };
226
+ }
227
+ if (code === 0) {
228
+ const cookies = cgiRes.cookie || [];
229
+ const cookieObj = parseSetCookie(cookies);
230
+ if (cgiBody.musicid && !cookieObj.uin) cookieObj.uin = String(cgiBody.musicid);
231
+ if (cgiBody.musickey && !cookieObj.qm_keyst) cookieObj.qm_keyst = String(cgiBody.musickey);
232
+ if (cgiBody.musickey && !cookieObj.qqmusic_key) cookieObj.qqmusic_key = String(cgiBody.musickey);
233
+ if (cgiBody.openid && !cookieObj.openid) cookieObj.openid = String(cgiBody.openid);
234
+ if (cgiBody.refresh_token && !cookieObj.refresh_token) cookieObj.refresh_token = String(cgiBody.refresh_token);
235
+
236
+ const cookieStr = cookieToString(cookieObj);
237
+ return {
238
+ status: 200,
239
+ body: {
240
+ code: 200,
241
+ message: "登录成功",
242
+ uin: cookieObj.uin || String(cgiBody.musicid || ""),
243
+ musickey: cookieObj.qm_keyst || String(cgiBody.musickey || ""),
244
+ cookieStr,
245
+ data: cgiBody,
246
+ },
247
+ cookie: cookies,
248
+ };
249
+ }
250
+
251
+ return {
252
+ status: 200,
253
+ body: {
254
+ code: 801,
255
+ message: cgiBody.msg || "等待扫码",
256
+ data: cgiBody,
257
+ },
258
+ };
259
+ }
260
+
182
261
  // 微信扫码轮询
183
262
  const { uuid } = query;
184
263
  if (!uuid) {
@@ -24,14 +24,17 @@ export default async function login_qr_create(query = {}, req = request) {
24
24
  const ptqrtoken = hash33(qrsig);
25
25
  // 二维码图片转 base64
26
26
  let qrimg = "";
27
- if (typeof res.body === "string") {
27
+ if (Buffer.isBuffer(res.body)) {
28
+ qrimg = `data:image/png;base64,${res.body.toString("base64")}`;
29
+ }
30
+ else if (res.body instanceof Uint8Array) {
28
31
  qrimg = `data:image/png;base64,${Buffer.from(res.body).toString("base64")}`;
29
32
  }
30
- else if (Buffer.isBuffer(res.body)) {
31
- qrimg = `data:image/png;base64,${res.body.toString("base64")}`;
33
+ else if (typeof res.body === "string") {
34
+ qrimg = res.body.startsWith("data:") ? res.body : `data:image/png;base64,${Buffer.from(res.body).toString("base64")}`;
32
35
  }
33
36
  else {
34
- qrimg = `data:image/png;base64,${Buffer.from(String(res.body)).toString("base64")}`;
37
+ qrimg = `data:image/png;base64,${Buffer.from(String(res.body || "")).toString("base64")}`;
35
38
  }
36
39
  return {
37
40
  status: 200,
@@ -34,12 +34,14 @@ export default async function login_qr_create(
34
34
 
35
35
  // 二维码图片转 base64
36
36
  let qrimg = "";
37
- if (typeof res.body === "string") {
38
- qrimg = `data:image/png;base64,${Buffer.from(res.body).toString("base64")}`;
39
- } else if (Buffer.isBuffer(res.body)) {
37
+ if (Buffer.isBuffer(res.body)) {
40
38
  qrimg = `data:image/png;base64,${res.body.toString("base64")}`;
39
+ } else if (res.body instanceof Uint8Array) {
40
+ qrimg = `data:image/png;base64,${Buffer.from(res.body).toString("base64")}`;
41
+ } else if (typeof res.body === "string") {
42
+ qrimg = res.body.startsWith("data:") ? res.body : `data:image/png;base64,${Buffer.from(res.body).toString("base64")}`;
41
43
  } else {
42
- qrimg = `data:image/png;base64,${Buffer.from(String(res.body)).toString("base64")}`;
44
+ qrimg = `data:image/png;base64,${Buffer.from(String(res.body || "")).toString("base64")}`;
43
45
  }
44
46
 
45
47
  return {
@@ -11,4 +11,8 @@ export default function login_refresh(query?: {
11
11
  openid?: string;
12
12
  refresh_token?: string;
13
13
  refresh_key?: string;
14
- }, req?: typeof request): Promise<ApiResponse>;
14
+ }, req?: typeof request): Promise<ApiResponse<{
15
+ code: number;
16
+ data: any;
17
+ cookieStr: string;
18
+ }>>;
@@ -1,5 +1,5 @@
1
1
  import { request } from "../util/request.js";
2
- import { cookieToJson } from "../util/cookie.js";
2
+ import { cookieToJson, cookieToString, parseSetCookie } from "../util/cookie.js";
3
3
  /**
4
4
  * 刷新登录凭证.
5
5
  *
@@ -24,11 +24,22 @@ export default async function login_refresh(query = {}, req = request) {
24
24
  cookie: query.cookie,
25
25
  platform: "web",
26
26
  });
27
+ const resBody = (cgiRes.body || {});
28
+ const newCookies = parseSetCookie(cgiRes.cookie || []);
29
+ const mergedCookies = { ...cookieObj, ...newCookies };
30
+ if (resBody.musickey && !mergedCookies.qm_keyst) {
31
+ mergedCookies.qm_keyst = resBody.musickey;
32
+ }
33
+ if (resBody.musickey && !mergedCookies.qqmusic_key) {
34
+ mergedCookies.qqmusic_key = resBody.musickey;
35
+ }
36
+ const cookieStr = cookieToString(mergedCookies);
27
37
  return {
28
38
  status: cgiRes.status,
29
39
  body: {
30
- code: cgiRes.body?.code ?? 200,
31
- data: cgiRes.body,
40
+ code: resBody.code ?? (cgiRes.status === 200 ? 200 : 500),
41
+ data: resBody,
42
+ cookieStr,
32
43
  },
33
44
  cookie: cgiRes.cookie,
34
45
  };
@@ -1,5 +1,5 @@
1
1
  import { request, type ApiResponse } from "../util/request.js";
2
- import { cookieToJson } from "../util/cookie.js";
2
+ import { cookieToJson, cookieToString, parseSetCookie } from "../util/cookie.js";
3
3
 
4
4
  /**
5
5
  * 刷新登录凭证.
@@ -16,7 +16,7 @@ export default async function login_refresh(
16
16
  refresh_key?: string;
17
17
  } = {},
18
18
  req = request
19
- ): Promise<ApiResponse> {
19
+ ): Promise<ApiResponse<{ code: number; data: any; cookieStr: string }>> {
20
20
  const cookieObj = cookieToJson(query.cookie);
21
21
  const uin = cookieObj.uin || cookieObj.qqmusic_uin || cookieObj.wxuin || "0";
22
22
  const musickey = cookieObj.qm_keyst || cookieObj.qqmusic_key || "";
@@ -41,11 +41,23 @@ export default async function login_refresh(
41
41
  }
42
42
  );
43
43
 
44
+ const resBody = (cgiRes.body || {}) as any;
45
+ const newCookies = parseSetCookie(cgiRes.cookie || []);
46
+ const mergedCookies = { ...cookieObj, ...newCookies };
47
+ if (resBody.musickey && !mergedCookies.qm_keyst) {
48
+ mergedCookies.qm_keyst = resBody.musickey;
49
+ }
50
+ if (resBody.musickey && !mergedCookies.qqmusic_key) {
51
+ mergedCookies.qqmusic_key = resBody.musickey;
52
+ }
53
+ const cookieStr = cookieToString(mergedCookies);
54
+
44
55
  return {
45
56
  status: cgiRes.status,
46
57
  body: {
47
- code: cgiRes.body?.code ?? 200,
48
- data: cgiRes.body,
58
+ code: resBody.code ?? (cgiRes.status === 200 ? 200 : 500),
59
+ data: resBody,
60
+ cookieStr,
49
61
  },
50
62
  cookie: cgiRes.cookie,
51
63
  };
@@ -1,9 +1,30 @@
1
1
  import { request, type ApiResponse } from "../util/request.js";
2
+ import { type NormalizedVipInfo } from "./user_vip_info.js";
3
+ export interface NormalizedLoginAccount {
4
+ userId: string;
5
+ nickname: string;
6
+ avatarUrl?: string;
7
+ vipStatus: "vip" | "nonVip" | "unknown";
8
+ vipTier?: "svip" | "vip";
9
+ vipLevel?: number;
10
+ vipYear?: boolean;
11
+ vipExpireTime?: number;
12
+ }
13
+ export interface LoginStatusResult {
14
+ code: number;
15
+ isLogin: boolean;
16
+ uin: string;
17
+ nickname: string;
18
+ avatarUrl?: string;
19
+ vip: NormalizedVipInfo;
20
+ account: NormalizedLoginAccount;
21
+ data: any;
22
+ }
2
23
  /**
3
- * 校验当前 Cookie 登录状态及获取用户信息.
24
+ * 校验当前 Cookie 登录状态及获取用户信息与 VIP 等级.
4
25
  *
5
26
  * @param query.cookie 登录凭据 Cookie
6
27
  */
7
28
  export default function login_status(query?: {
8
29
  cookie?: any;
9
- }, req?: typeof request): Promise<ApiResponse>;
30
+ }, req?: typeof request): Promise<ApiResponse<LoginStatusResult>>;
@@ -1,21 +1,76 @@
1
1
  import { request } from "../util/request.js";
2
+ import { cookieToJson } from "../util/cookie.js";
3
+ import user_detail_fn from "./user_detail.js";
4
+ import user_vip_info_fn from "./user_vip_info.js";
2
5
  /**
3
- * 校验当前 Cookie 登录状态及获取用户信息.
6
+ * 校验当前 Cookie 登录状态及获取用户信息与 VIP 等级.
4
7
  *
5
8
  * @param query.cookie 登录凭据 Cookie
6
9
  */
7
10
  export default async function login_status(query = {}, req = request) {
11
+ const cookieObj = cookieToJson(query.cookie);
12
+ let uin = cookieObj.uin || cookieObj.qqmusic_uin || cookieObj.wxuin || "";
8
13
  const cgiRes = await req("music.UserInfo.userInfoServer", "GetLoginUserInfo", {}, {
9
14
  cookie: query.cookie,
10
15
  platform: "web",
11
16
  });
12
- const data = cgiRes.body || {};
13
- const isLogin = data.code === 0 && Boolean(data.user?.uin || data.uin || data.encryptUin);
17
+ const data = (cgiRes.body || {});
18
+ const userData = data.user || data.userInfo || data;
19
+ const isLogin = data.code === 0 && Boolean(userData.uin || userData.encryptUin || uin);
20
+ if (userData.uin) {
21
+ uin = String(userData.uin).trim();
22
+ }
23
+ else if (!uin && userData.encryptUin) {
24
+ uin = String(userData.encryptUin).trim();
25
+ }
26
+ let nickname = userData.nick || userData.nickname || "";
27
+ let avatarUrl = userData.headurl || userData.avatar;
28
+ // 若基本信息不全且处于已登录状态,尝试从 user_detail 补全昵称与头像
29
+ if (isLogin && (!nickname || !avatarUrl)) {
30
+ try {
31
+ const detailRes = await user_detail_fn({ uin, cookie: query.cookie }, req);
32
+ const header = detailRes.body?.data?.data || detailRes.body?.data || {};
33
+ if (header.nick && !nickname)
34
+ nickname = header.nick;
35
+ if (header.headurl && !avatarUrl)
36
+ avatarUrl = header.headurl;
37
+ }
38
+ catch { }
39
+ }
40
+ if (!nickname) {
41
+ nickname = uin ? `QQ用户_${uin.slice(-4)}` : "QQ音乐用户";
42
+ }
43
+ // 补全 VIP 会员信息
44
+ let vip = { vipStatus: "nonVip" };
45
+ if (isLogin) {
46
+ try {
47
+ const vipRes = await user_vip_info_fn({ cookie: query.cookie }, req);
48
+ vip = vipRes.body.vip;
49
+ }
50
+ catch {
51
+ vip = { vipStatus: "unknown" };
52
+ }
53
+ }
54
+ const account = {
55
+ userId: uin,
56
+ nickname,
57
+ avatarUrl,
58
+ vipStatus: vip.vipStatus,
59
+ vipTier: vip.vipTier,
60
+ vipLevel: vip.vipLevel,
61
+ vipYear: vip.vipYear,
62
+ vipExpireTime: vip.vipExpireTime,
63
+ };
14
64
  return {
15
65
  status: cgiRes.status,
16
66
  body: {
17
67
  code: isLogin ? 200 : 301,
18
68
  isLogin,
69
+ uin,
70
+ nickname,
71
+ avatarUrl,
72
+ vip,
73
+ account,
19
74
  data,
20
75
  },
21
76
  cookie: cgiRes.cookie,
@@ -1,14 +1,42 @@
1
1
  import { request, type ApiResponse } from "../util/request.js";
2
+ import { cookieToJson } from "../util/cookie.js";
3
+ import user_detail_fn from "./user_detail.js";
4
+ import user_vip_info_fn, { type NormalizedVipInfo } from "./user_vip_info.js";
5
+
6
+ export interface NormalizedLoginAccount {
7
+ userId: string;
8
+ nickname: string;
9
+ avatarUrl?: string;
10
+ vipStatus: "vip" | "nonVip" | "unknown";
11
+ vipTier?: "svip" | "vip";
12
+ vipLevel?: number;
13
+ vipYear?: boolean;
14
+ vipExpireTime?: number;
15
+ }
16
+
17
+ export interface LoginStatusResult {
18
+ code: number;
19
+ isLogin: boolean;
20
+ uin: string;
21
+ nickname: string;
22
+ avatarUrl?: string;
23
+ vip: NormalizedVipInfo;
24
+ account: NormalizedLoginAccount;
25
+ data: any;
26
+ }
2
27
 
3
28
  /**
4
- * 校验当前 Cookie 登录状态及获取用户信息.
29
+ * 校验当前 Cookie 登录状态及获取用户信息与 VIP 等级.
5
30
  *
6
31
  * @param query.cookie 登录凭据 Cookie
7
32
  */
8
33
  export default async function login_status(
9
34
  query: { cookie?: any } = {},
10
35
  req = request
11
- ): Promise<ApiResponse> {
36
+ ): Promise<ApiResponse<LoginStatusResult>> {
37
+ const cookieObj = cookieToJson(query.cookie);
38
+ let uin = cookieObj.uin || cookieObj.qqmusic_uin || cookieObj.wxuin || "";
39
+
12
40
  const cgiRes = await req(
13
41
  "music.UserInfo.userInfoServer",
14
42
  "GetLoginUserInfo",
@@ -19,14 +47,65 @@ export default async function login_status(
19
47
  }
20
48
  );
21
49
 
22
- const data = cgiRes.body || {};
23
- const isLogin = data.code === 0 && Boolean(data.user?.uin || data.uin || data.encryptUin);
50
+ const data = (cgiRes.body || {}) as any;
51
+ const userData = data.user || data.userInfo || data;
52
+ const isLogin = data.code === 0 && Boolean(userData.uin || userData.encryptUin || uin);
53
+
54
+ if (userData.uin) {
55
+ uin = String(userData.uin).trim();
56
+ } else if (!uin && userData.encryptUin) {
57
+ uin = String(userData.encryptUin).trim();
58
+ }
59
+
60
+ let nickname = userData.nick || userData.nickname || "";
61
+ let avatarUrl: string | undefined = userData.headurl || userData.avatar;
62
+
63
+ // 若基本信息不全且处于已登录状态,尝试从 user_detail 补全昵称与头像
64
+ if (isLogin && (!nickname || !avatarUrl)) {
65
+ try {
66
+ const detailRes = await user_detail_fn({ uin, cookie: query.cookie }, req);
67
+ const header = detailRes.body?.data?.data || detailRes.body?.data || {};
68
+ if (header.nick && !nickname) nickname = header.nick;
69
+ if (header.headurl && !avatarUrl) avatarUrl = header.headurl;
70
+ } catch {}
71
+ }
72
+
73
+ if (!nickname) {
74
+ nickname = uin ? `QQ用户_${uin.slice(-4)}` : "QQ音乐用户";
75
+ }
76
+
77
+ // 补全 VIP 会员信息
78
+ let vip: NormalizedVipInfo = { vipStatus: "nonVip" };
79
+ if (isLogin) {
80
+ try {
81
+ const vipRes = await user_vip_info_fn({ cookie: query.cookie }, req);
82
+ vip = vipRes.body.vip;
83
+ } catch {
84
+ vip = { vipStatus: "unknown" };
85
+ }
86
+ }
87
+
88
+ const account: NormalizedLoginAccount = {
89
+ userId: uin,
90
+ nickname,
91
+ avatarUrl,
92
+ vipStatus: vip.vipStatus,
93
+ vipTier: vip.vipTier,
94
+ vipLevel: vip.vipLevel,
95
+ vipYear: vip.vipYear,
96
+ vipExpireTime: vip.vipExpireTime,
97
+ };
24
98
 
25
99
  return {
26
100
  status: cgiRes.status,
27
101
  body: {
28
102
  code: isLogin ? 200 : 301,
29
103
  isLogin,
104
+ uin,
105
+ nickname,
106
+ avatarUrl,
107
+ vip,
108
+ account,
30
109
  data,
31
110
  },
32
111
  cookie: cgiRes.cookie,
@@ -1,4 +1,12 @@
1
1
  import { request, type ApiResponse } from "../util/request.js";
2
+ export interface NormalizedVipInfo {
3
+ vipStatus: "vip" | "nonVip" | "unknown";
4
+ vipTier?: "svip" | "vip";
5
+ vipLevel?: number;
6
+ vipYear?: boolean;
7
+ vipExpireTime?: number;
8
+ }
9
+ export declare function deriveVipInfo(body: any): NormalizedVipInfo;
2
10
  /**
3
11
  * 获取当前登录用户的 VIP 会员信息及身份明细.
4
12
  *
@@ -6,4 +14,8 @@ import { request, type ApiResponse } from "../util/request.js";
6
14
  */
7
15
  export default function user_vip_info(query?: {
8
16
  cookie?: any;
9
- }, req?: typeof request): Promise<ApiResponse>;
17
+ }, req?: typeof request): Promise<ApiResponse<{
18
+ code: number;
19
+ data: any;
20
+ vip: NormalizedVipInfo;
21
+ }>>;
@@ -1,4 +1,54 @@
1
1
  import { request } from "../util/request.js";
2
+ export function deriveVipInfo(body) {
3
+ if (!body) {
4
+ return { vipStatus: "unknown" };
5
+ }
6
+ const vipData = body.data || body;
7
+ const identity = vipData.identity || {};
8
+ const svip = Number(vipData.svip || 0);
9
+ const isVip = Number(identity.HugeVip || identity.vip || 0);
10
+ const level = Number(identity.level || 0);
11
+ const isYear = Boolean(Number(identity.is_year || identity.year_vip || identity.HugeVipYear || identity.year || 0) > 0 ||
12
+ Number(vipData.year || vipData.is_year || 0) > 0);
13
+ let vipStatus = "nonVip";
14
+ let vipTier = undefined;
15
+ let vipLevel = undefined;
16
+ let vipYear = undefined;
17
+ let vipExpireTime = undefined;
18
+ if (svip > 0) {
19
+ vipStatus = "vip";
20
+ vipTier = "svip";
21
+ vipLevel = level > 0 ? Math.min(Math.max(level, 1), 10) : 1;
22
+ vipYear = isYear;
23
+ }
24
+ else if (isVip > 0) {
25
+ vipStatus = "vip";
26
+ vipTier = "vip";
27
+ vipLevel = level > 0 ? Math.min(Math.max(level, 1), 10) : 1;
28
+ vipYear = isYear;
29
+ }
30
+ const expireTs = Number(vipData.userinfo?.expire || 0);
31
+ if (expireTs > 0) {
32
+ vipExpireTime = expireTs * 1000;
33
+ }
34
+ else if (identity.HugeVipEnd) {
35
+ const parsed = Date.parse(identity.HugeVipEnd);
36
+ if (!isNaN(parsed))
37
+ vipExpireTime = parsed;
38
+ }
39
+ else if (identity.twelveEnd) {
40
+ const parsed = Date.parse(identity.twelveEnd);
41
+ if (!isNaN(parsed))
42
+ vipExpireTime = parsed;
43
+ }
44
+ return {
45
+ vipStatus,
46
+ vipTier,
47
+ vipLevel,
48
+ vipYear,
49
+ vipExpireTime,
50
+ };
51
+ }
2
52
  /**
3
53
  * 获取当前登录用户的 VIP 会员信息及身份明细.
4
54
  *
@@ -9,11 +59,13 @@ export default async function user_vip_info(query = {}, req = request) {
9
59
  cookie: query.cookie,
10
60
  platform: "web",
11
61
  });
62
+ const vip = deriveVipInfo(cgiRes.body);
12
63
  return {
13
64
  status: cgiRes.status,
14
65
  body: {
15
66
  code: cgiRes.body?.code ?? 200,
16
67
  data: cgiRes.body,
68
+ vip,
17
69
  },
18
70
  cookie: cgiRes.cookie,
19
71
  };
@@ -1,5 +1,65 @@
1
1
  import { request, type ApiResponse } from "../util/request.js";
2
2
 
3
+ export interface NormalizedVipInfo {
4
+ vipStatus: "vip" | "nonVip" | "unknown";
5
+ vipTier?: "svip" | "vip";
6
+ vipLevel?: number;
7
+ vipYear?: boolean;
8
+ vipExpireTime?: number;
9
+ }
10
+
11
+ export function deriveVipInfo(body: any): NormalizedVipInfo {
12
+ if (!body) {
13
+ return { vipStatus: "unknown" };
14
+ }
15
+ const vipData = body.data || body;
16
+ const identity = vipData.identity || {};
17
+ const svip = Number(vipData.svip || 0);
18
+ const isVip = Number(identity.HugeVip || identity.vip || 0);
19
+ const level = Number(identity.level || 0);
20
+ const isYear = Boolean(
21
+ Number(identity.is_year || identity.year_vip || identity.HugeVipYear || identity.year || 0) > 0 ||
22
+ Number(vipData.year || vipData.is_year || 0) > 0
23
+ );
24
+
25
+ let vipStatus: "vip" | "nonVip" | "unknown" = "nonVip";
26
+ let vipTier: "svip" | "vip" | undefined = undefined;
27
+ let vipLevel: number | undefined = undefined;
28
+ let vipYear: boolean | undefined = undefined;
29
+ let vipExpireTime: number | undefined = undefined;
30
+
31
+ if (svip > 0) {
32
+ vipStatus = "vip";
33
+ vipTier = "svip";
34
+ vipLevel = level > 0 ? Math.min(Math.max(level, 1), 10) : 1;
35
+ vipYear = isYear;
36
+ } else if (isVip > 0) {
37
+ vipStatus = "vip";
38
+ vipTier = "vip";
39
+ vipLevel = level > 0 ? Math.min(Math.max(level, 1), 10) : 1;
40
+ vipYear = isYear;
41
+ }
42
+
43
+ const expireTs = Number(vipData.userinfo?.expire || 0);
44
+ if (expireTs > 0) {
45
+ vipExpireTime = expireTs * 1000;
46
+ } else if (identity.HugeVipEnd) {
47
+ const parsed = Date.parse(identity.HugeVipEnd);
48
+ if (!isNaN(parsed)) vipExpireTime = parsed;
49
+ } else if (identity.twelveEnd) {
50
+ const parsed = Date.parse(identity.twelveEnd);
51
+ if (!isNaN(parsed)) vipExpireTime = parsed;
52
+ }
53
+
54
+ return {
55
+ vipStatus,
56
+ vipTier,
57
+ vipLevel,
58
+ vipYear,
59
+ vipExpireTime,
60
+ };
61
+ }
62
+
3
63
  /**
4
64
  * 获取当前登录用户的 VIP 会员信息及身份明细.
5
65
  *
@@ -8,7 +68,7 @@ import { request, type ApiResponse } from "../util/request.js";
8
68
  export default async function user_vip_info(
9
69
  query: { cookie?: any } = {},
10
70
  req = request
11
- ): Promise<ApiResponse> {
71
+ ): Promise<ApiResponse<{ code: number; data: any; vip: NormalizedVipInfo }>> {
12
72
  const cgiRes = await req(
13
73
  "VipLogin.VipLoginInter",
14
74
  "vip_login_base",
@@ -19,11 +79,14 @@ export default async function user_vip_info(
19
79
  }
20
80
  );
21
81
 
82
+ const vip = deriveVipInfo(cgiRes.body);
83
+
22
84
  return {
23
85
  status: cgiRes.status,
24
86
  body: {
25
87
  code: cgiRes.body?.code ?? 200,
26
88
  data: cgiRes.body,
89
+ vip,
27
90
  },
28
91
  cookie: cgiRes.cookie,
29
92
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qqmusic-api-enhanced",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "QQ音乐增强版API,支持扫码登录/凭证刷新/直链提取/歌词解密/歌单歌手检索 || QQ Music Enhanced API for Node.js & Bun",
5
5
  "type": "module",
6
6
  "main": "main.js",
package/util/request.js CHANGED
@@ -75,6 +75,28 @@ export async function requestCgi(module, method, param = {}, options = {}) {
75
75
  clearTimeout(timeoutTimer);
76
76
  }
77
77
  }
78
+ function isBinaryBuffer(buf, contentType) {
79
+ if (contentType.includes("image/") ||
80
+ contentType.includes("application/octet-stream") ||
81
+ contentType.includes("audio/") ||
82
+ contentType.includes("video/")) {
83
+ return true;
84
+ }
85
+ if (buf.length >= 4) {
86
+ if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47)
87
+ return true; // PNG
88
+ if (buf[0] === 0xff && buf[1] === 0xd8)
89
+ return true; // JPEG
90
+ if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46)
91
+ return true; // GIF
92
+ if (buf.length >= 12 &&
93
+ buf.subarray(0, 4).toString("ascii") === "RIFF" &&
94
+ buf.subarray(8, 12).toString("ascii") === "WEBP") {
95
+ return true; // WEBP
96
+ }
97
+ }
98
+ return false;
99
+ }
78
100
  /**
79
101
  * 执行原始 HTTP/HTTPS 请求 (用于扫码、验证码或第三方 OAuth 接口).
80
102
  *
@@ -90,7 +112,19 @@ export async function requestRaw(url, init = {}) {
90
112
  ...init,
91
113
  signal: controller.signal,
92
114
  });
93
- const text = await res.text();
115
+ const cookie = res.headers.getSetCookie?.() || [];
116
+ const contentType = res.headers.get("content-type") || "";
117
+ const buffer = Buffer.from(await res.arrayBuffer());
118
+ if (isBinaryBuffer(buffer, contentType)) {
119
+ return {
120
+ status: res.status,
121
+ data: buffer,
122
+ text: "",
123
+ headers: res.headers,
124
+ cookie,
125
+ };
126
+ }
127
+ const text = buffer.toString("utf-8");
94
128
  let data = text;
95
129
  try {
96
130
  data = JSON.parse(text);
@@ -98,7 +132,6 @@ export async function requestRaw(url, init = {}) {
98
132
  catch {
99
133
  // 保持原始文本
100
134
  }
101
- const cookie = res.headers.getSetCookie?.() || [];
102
135
  return {
103
136
  status: res.status,
104
137
  data,
package/util/request.ts CHANGED
@@ -104,6 +104,30 @@ export async function requestCgi<T = any>(
104
104
  }
105
105
  }
106
106
 
107
+ function isBinaryBuffer(buf: Buffer, contentType: string): boolean {
108
+ if (
109
+ contentType.includes("image/") ||
110
+ contentType.includes("application/octet-stream") ||
111
+ contentType.includes("audio/") ||
112
+ contentType.includes("video/")
113
+ ) {
114
+ return true;
115
+ }
116
+ if (buf.length >= 4) {
117
+ if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) return true; // PNG
118
+ if (buf[0] === 0xff && buf[1] === 0xd8) return true; // JPEG
119
+ if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) return true; // GIF
120
+ if (
121
+ buf.length >= 12 &&
122
+ buf.subarray(0, 4).toString("ascii") === "RIFF" &&
123
+ buf.subarray(8, 12).toString("ascii") === "WEBP"
124
+ ) {
125
+ return true; // WEBP
126
+ }
127
+ }
128
+ return false;
129
+ }
130
+
107
131
  /**
108
132
  * 执行原始 HTTP/HTTPS 请求 (用于扫码、验证码或第三方 OAuth 接口).
109
133
  *
@@ -124,7 +148,21 @@ export async function requestRaw(
124
148
  signal: controller.signal,
125
149
  });
126
150
 
127
- const text = await res.text();
151
+ const cookie = res.headers.getSetCookie?.() || [];
152
+ const contentType = res.headers.get("content-type") || "";
153
+ const buffer = Buffer.from(await res.arrayBuffer());
154
+
155
+ if (isBinaryBuffer(buffer, contentType)) {
156
+ return {
157
+ status: res.status,
158
+ data: buffer,
159
+ text: "",
160
+ headers: res.headers,
161
+ cookie,
162
+ };
163
+ }
164
+
165
+ const text = buffer.toString("utf-8");
128
166
  let data: any = text;
129
167
  try {
130
168
  data = JSON.parse(text);
@@ -132,7 +170,6 @@ export async function requestRaw(
132
170
  // 保持原始文本
133
171
  }
134
172
 
135
- const cookie = res.headers.getSetCookie?.() || [];
136
173
  return {
137
174
  status: res.status,
138
175
  data,