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 +57 -4
- package/main.js +5 -1
- package/main.ts +17 -1
- package/module/login_qr_check.d.ts +5 -2
- package/module/login_qr_check.js +72 -1
- package/module/login_qr_check.ts +81 -2
- package/module/login_qr_create.js +7 -4
- package/module/login_qr_create.ts +6 -4
- package/module/login_refresh.d.ts +5 -1
- package/module/login_refresh.js +14 -3
- package/module/login_refresh.ts +16 -4
- package/module/login_status.d.ts +23 -2
- package/module/login_status.js +58 -3
- package/module/login_status.ts +83 -4
- package/module/user_vip_info.d.ts +13 -1
- package/module/user_vip_info.js +52 -0
- package/module/user_vip_info.ts +64 -1
- package/package.json +1 -1
- package/util/request.js +35 -2
- package/util/request.ts +39 -2
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
|
-
|
|
38
|
-
|
|
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
|
|
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
|
|
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
|
|
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>;
|
package/module/login_qr_check.js
CHANGED
|
@@ -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) {
|
package/module/login_qr_check.ts
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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 (
|
|
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
|
+
}>>;
|
package/module/login_refresh.js
CHANGED
|
@@ -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:
|
|
31
|
-
data:
|
|
40
|
+
code: resBody.code ?? (cgiRes.status === 200 ? 200 : 500),
|
|
41
|
+
data: resBody,
|
|
42
|
+
cookieStr,
|
|
32
43
|
},
|
|
33
44
|
cookie: cgiRes.cookie,
|
|
34
45
|
};
|
package/module/login_refresh.ts
CHANGED
|
@@ -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:
|
|
48
|
-
data:
|
|
58
|
+
code: resBody.code ?? (cgiRes.status === 200 ? 200 : 500),
|
|
59
|
+
data: resBody,
|
|
60
|
+
cookieStr,
|
|
49
61
|
},
|
|
50
62
|
cookie: cgiRes.cookie,
|
|
51
63
|
};
|
package/module/login_status.d.ts
CHANGED
|
@@ -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>>;
|
package/module/login_status.js
CHANGED
|
@@ -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
|
|
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,
|
package/module/login_status.ts
CHANGED
|
@@ -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
|
|
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
|
+
}>>;
|
package/module/user_vip_info.js
CHANGED
|
@@ -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
|
};
|
package/module/user_vip_info.ts
CHANGED
|
@@ -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
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
|
|
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
|
|
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,
|