qqmusic-api-enhanced 1.0.2 → 1.0.4

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.
@@ -7,7 +7,7 @@ export interface NormalizedVipInfo {
7
7
  vipExpireTime?: number;
8
8
  }
9
9
  export declare function parseVipFromIcons(iconList?: any[], icons?: any[]): Partial<NormalizedVipInfo> | null;
10
- export declare function deriveVipInfo(body: any): NormalizedVipInfo;
10
+ export declare function deriveVipInfo(body: any, now?: number): NormalizedVipInfo;
11
11
  /**
12
12
  * 获取当前登录用户的 VIP 会员信息及身份明细.
13
13
  *
@@ -2,9 +2,33 @@ import { request } from "../util/request.js";
2
2
  import { cookieToJson } from "../util/cookie.js";
3
3
  import user_detail_fn from "./user_detail.js";
4
4
  export function parseVipFromIcons(iconList = [], icons = []) {
5
- for (const item of [...iconList, ...icons]) {
5
+ // 1. 优先从用户主页活跃佩戴的 iconList (NewIconInfo.iconlist) 查找
6
+ for (const item of iconList) {
6
7
  if (!item)
7
8
  continue;
9
+ if (item.GreyURL && String(item.GreyURL).trim().length > 0)
10
+ continue;
11
+ const url = String(item.srcUrl || item.IconURL || "");
12
+ const helptxt = String(item.Helptxt || item.Title || "");
13
+ const m = url.match(/(?:^|\/)(n)?(s?vip)(\d+)\.(?:png|webp)/i);
14
+ if (m) {
15
+ const isYear = Boolean(m[1]) || helptxt.includes("年费");
16
+ const tier = m[2].toLowerCase() === "svip" ? "svip" : "vip";
17
+ const level = parseInt(m[3], 10);
18
+ return {
19
+ vipStatus: "vip",
20
+ vipTier: tier,
21
+ vipLevel: level > 0 ? Math.min(Math.max(level, 1), 10) : 1,
22
+ vipYear: isYear,
23
+ };
24
+ }
25
+ }
26
+ // 2. 其次才从旧版快捷入口 icons 查找
27
+ for (const item of icons) {
28
+ if (!item)
29
+ continue;
30
+ if (item.GreyURL && String(item.GreyURL).trim().length > 0)
31
+ continue;
8
32
  const url = String(item.srcUrl || item.IconURL || "");
9
33
  const helptxt = String(item.Helptxt || item.Title || "");
10
34
  const m = url.match(/(?:^|\/)(n)?(s?vip)(\d+)\.(?:png|webp)/i);
@@ -22,7 +46,27 @@ export function parseVipFromIcons(iconList = [], icons = []) {
22
46
  }
23
47
  return null;
24
48
  }
25
- export function deriveVipInfo(body) {
49
+ function parseDateMs(val) {
50
+ if (val === undefined || val === null || val === "" || val === 0)
51
+ return undefined;
52
+ if (typeof val === "number" && val > 0) {
53
+ return val > 1e11 ? val : val * 1000;
54
+ }
55
+ if (typeof val === "string") {
56
+ const trimmed = val.trim();
57
+ if (!trimmed)
58
+ return undefined;
59
+ const num = Number(trimmed);
60
+ if (!isNaN(num) && num > 0) {
61
+ return num > 1e11 ? num : num * 1000;
62
+ }
63
+ const parsed = Date.parse(trimmed.replace(/-/g, "/"));
64
+ if (!isNaN(parsed))
65
+ return parsed;
66
+ }
67
+ return undefined;
68
+ }
69
+ export function deriveVipInfo(body, now = Date.now()) {
26
70
  if (!body) {
27
71
  return { vipStatus: "unknown" };
28
72
  }
@@ -33,24 +77,56 @@ export function deriveVipInfo(body) {
33
77
  const level = Number(identity.level || 0);
34
78
  const isYear = Boolean(Number(identity.HugeYearFlag || identity.yearflag || identity.is_year || identity.year_vip || 0) > 0 ||
35
79
  Number(vipData.ystar || vipData.year || 0) > 0);
80
+ // 1. 分别提取 SVIP 专有到期时间
81
+ let svipExpire = undefined;
82
+ if (vipData.starend)
83
+ svipExpire = parseDateMs(vipData.starend);
84
+ else if (vipData.ystarend)
85
+ svipExpire = parseDateMs(vipData.ystarend);
86
+ else if (identity.SvipEnd || identity.svip_end)
87
+ svipExpire = parseDateMs(identity.SvipEnd || identity.svip_end);
88
+ else if (svip > 0 && Number(vipData.userinfo?.expire || 0) > 0) {
89
+ svipExpire = parseDateMs(vipData.userinfo.expire);
90
+ }
91
+ // 2. 分别提取普通 VIP / 豪华绿钻专有到期时间
92
+ let vipExpire = undefined;
93
+ if (identity.HugeVipEnd)
94
+ vipExpire = parseDateMs(identity.HugeVipEnd);
95
+ else if (identity.twelveEnd)
96
+ vipExpire = parseDateMs(identity.twelveEnd);
97
+ else if (identity.eightEnd)
98
+ vipExpire = parseDateMs(identity.eightEnd);
99
+ else if (identity.overdate)
100
+ vipExpire = parseDateMs(identity.overdate);
101
+ else if (vipData.mygreen)
102
+ vipExpire = parseDateMs(vipData.mygreen);
103
+ else if (svip === 0 && Number(vipData.userinfo?.expire || 0) > 0) {
104
+ vipExpire = parseDateMs(vipData.userinfo.expire);
105
+ }
106
+ const isSvipExpired = svipExpire !== undefined && svipExpire <= now;
107
+ const isVipExpired = vipExpire !== undefined && vipExpire <= now;
36
108
  let vipStatus = "nonVip";
37
109
  let vipTier = undefined;
38
110
  let vipLevel = undefined;
39
111
  let vipYear = undefined;
40
112
  let vipExpireTime = undefined;
41
- if (svip > 0) {
113
+ // 1. 如果存在未过期的 SVIP
114
+ if (svip > 0 && !isSvipExpired) {
42
115
  vipStatus = "vip";
43
116
  vipTier = "svip";
44
117
  vipLevel = level > 0 ? Math.min(Math.max(level, 1), 10) : 1;
45
118
  vipYear = isYear;
119
+ vipExpireTime = svipExpire ?? (vipExpire && !isVipExpired ? vipExpire : undefined);
46
120
  }
47
- else if (isVip > 0) {
121
+ else if (isVip > 0 && !isVipExpired) {
122
+ // 2. 如果存在普通 VIP(若 SVIP 已过期但 VIP 未过期,降级并展示未过期的 VIP 到期时间)
48
123
  vipStatus = "vip";
49
124
  vipTier = "vip";
50
125
  vipLevel = level > 0 ? Math.min(Math.max(level, 1), 10) : 1;
51
126
  vipYear = isYear;
127
+ vipExpireTime = vipExpire ?? (svipExpire && !isSvipExpired ? svipExpire : undefined);
52
128
  }
53
- // 若基本 identity 未命中 VIP,尝试从附加的 Info / NewIconInfo / Icons 解析
129
+ // 3. 若 identity 未命中有效 VIP(例如 SVIP 已过期,且普通 VIP 缺少基础标志或需通过勋章校准)
54
130
  if (vipStatus === "nonVip") {
55
131
  const info = vipData.Info || vipData.info || {};
56
132
  const iconList = info.NewIconInfo?.iconlist || vipData.NewIconInfo?.iconlist || [];
@@ -61,21 +137,20 @@ export function deriveVipInfo(body) {
61
137
  vipTier = iconVip.vipTier;
62
138
  vipLevel = iconVip.vipLevel;
63
139
  vipYear = iconVip.vipYear;
140
+ if (iconVip.vipTier === "vip" && vipExpire && vipExpire > now) {
141
+ vipExpireTime = vipExpire;
142
+ }
143
+ else if (iconVip.vipTier === "svip" && svipExpire && svipExpire > now) {
144
+ vipExpireTime = svipExpire;
145
+ }
146
+ else {
147
+ vipExpireTime = undefined;
148
+ }
64
149
  }
65
150
  }
66
- const expireTs = Number(vipData.userinfo?.expire || 0);
67
- if (expireTs > 0) {
68
- vipExpireTime = expireTs * 1000;
69
- }
70
- else if (identity.HugeVipEnd) {
71
- const parsed = Date.parse(identity.HugeVipEnd);
72
- if (!isNaN(parsed))
73
- vipExpireTime = parsed;
74
- }
75
- else if (identity.twelveEnd) {
76
- const parsed = Date.parse(identity.twelveEnd);
77
- if (!isNaN(parsed))
78
- vipExpireTime = parsed;
151
+ // 4. 严苛防线:任何已过期的到期时间绝对不允许外露
152
+ if (vipExpireTime !== undefined && vipExpireTime <= now) {
153
+ vipExpireTime = undefined;
79
154
  }
80
155
  return {
81
156
  vipStatus,
@@ -108,17 +183,21 @@ export default async function user_vip_info(query = {}, req = request) {
108
183
  }
109
184
  catch { }
110
185
  let vip = deriveVipInfo(cgiRes.body);
111
- // 若 vip_login_base 无法推断出有效 VIP,自动通过主页头部接口获取勋章母版特征
112
- if (vip.vipStatus === "nonVip" && uin && uin !== "0") {
186
+ // 若 vip_login_base 无法推断出有效 VIP,或推断出的 SVIP 已经过期,或需要主页勋章校准,
187
+ // 自动通过主页头部接口获取勋章母版特征
188
+ if ((vip.vipStatus === "nonVip" || vip.vipTier === "svip") && uin && uin !== "0") {
113
189
  try {
114
190
  const detailRes = await user_detail_fn({ uin, cookie: query.cookie }, req);
115
191
  const detailData = detailRes.body?.data || {};
116
192
  const fallbackVip = deriveVipInfo(detailData);
117
193
  if (fallbackVip.vipStatus === "vip") {
118
- vip = {
119
- ...vip,
120
- ...fallbackVip,
121
- };
194
+ if (vip.vipStatus === "nonVip" || fallbackVip.vipTier === "vip" || !vip.vipExpireTime || vip.vipExpireTime <= Date.now()) {
195
+ vip = {
196
+ ...vip,
197
+ ...fallbackVip,
198
+ vipExpireTime: (vip.vipExpireTime && vip.vipExpireTime > Date.now()) ? vip.vipExpireTime : fallbackVip.vipExpireTime,
199
+ };
200
+ }
122
201
  }
123
202
  }
124
203
  catch { }
@@ -11,8 +11,30 @@ export interface NormalizedVipInfo {
11
11
  }
12
12
 
13
13
  export function parseVipFromIcons(iconList: any[] = [], icons: any[] = []): Partial<NormalizedVipInfo> | null {
14
- for (const item of [...iconList, ...icons]) {
14
+ // 1. 优先从用户主页活跃佩戴的 iconList (NewIconInfo.iconlist) 查找
15
+ for (const item of iconList) {
15
16
  if (!item) continue;
17
+ if (item.GreyURL && String(item.GreyURL).trim().length > 0) continue;
18
+ const url = String(item.srcUrl || item.IconURL || "");
19
+ const helptxt = String(item.Helptxt || item.Title || "");
20
+ const m = url.match(/(?:^|\/)(n)?(s?vip)(\d+)\.(?:png|webp)/i);
21
+ if (m) {
22
+ const isYear = Boolean(m[1]) || helptxt.includes("年费");
23
+ const tier = m[2].toLowerCase() === "svip" ? "svip" : "vip";
24
+ const level = parseInt(m[3], 10);
25
+ return {
26
+ vipStatus: "vip",
27
+ vipTier: tier,
28
+ vipLevel: level > 0 ? Math.min(Math.max(level, 1), 10) : 1,
29
+ vipYear: isYear,
30
+ };
31
+ }
32
+ }
33
+
34
+ // 2. 其次才从旧版快捷入口 icons 查找
35
+ for (const item of icons) {
36
+ if (!item) continue;
37
+ if (item.GreyURL && String(item.GreyURL).trim().length > 0) continue;
16
38
  const url = String(item.srcUrl || item.IconURL || "");
17
39
  const helptxt = String(item.Helptxt || item.Title || "");
18
40
  const m = url.match(/(?:^|\/)(n)?(s?vip)(\d+)\.(?:png|webp)/i);
@@ -31,7 +53,25 @@ export function parseVipFromIcons(iconList: any[] = [], icons: any[] = []): Part
31
53
  return null;
32
54
  }
33
55
 
34
- export function deriveVipInfo(body: any): NormalizedVipInfo {
56
+ function parseDateMs(val: any): number | undefined {
57
+ if (val === undefined || val === null || val === "" || val === 0) return undefined;
58
+ if (typeof val === "number" && val > 0) {
59
+ return val > 1e11 ? val : val * 1000;
60
+ }
61
+ if (typeof val === "string") {
62
+ const trimmed = val.trim();
63
+ if (!trimmed) return undefined;
64
+ const num = Number(trimmed);
65
+ if (!isNaN(num) && num > 0) {
66
+ return num > 1e11 ? num : num * 1000;
67
+ }
68
+ const parsed = Date.parse(trimmed.replace(/-/g, "/"));
69
+ if (!isNaN(parsed)) return parsed;
70
+ }
71
+ return undefined;
72
+ }
73
+
74
+ export function deriveVipInfo(body: any, now = Date.now()): NormalizedVipInfo {
35
75
  if (!body) {
36
76
  return { vipStatus: "unknown" };
37
77
  }
@@ -45,25 +85,52 @@ export function deriveVipInfo(body: any): NormalizedVipInfo {
45
85
  Number(vipData.ystar || vipData.year || 0) > 0
46
86
  );
47
87
 
88
+ // 1. 分别提取 SVIP 专有到期时间
89
+ let svipExpire: number | undefined = undefined;
90
+ if (vipData.starend) svipExpire = parseDateMs(vipData.starend);
91
+ else if (vipData.ystarend) svipExpire = parseDateMs(vipData.ystarend);
92
+ else if (identity.SvipEnd || identity.svip_end) svipExpire = parseDateMs(identity.SvipEnd || identity.svip_end);
93
+ else if (svip > 0 && Number(vipData.userinfo?.expire || 0) > 0) {
94
+ svipExpire = parseDateMs(vipData.userinfo.expire);
95
+ }
96
+
97
+ // 2. 分别提取普通 VIP / 豪华绿钻专有到期时间
98
+ let vipExpire: number | undefined = undefined;
99
+ if (identity.HugeVipEnd) vipExpire = parseDateMs(identity.HugeVipEnd);
100
+ else if (identity.twelveEnd) vipExpire = parseDateMs(identity.twelveEnd);
101
+ else if (identity.eightEnd) vipExpire = parseDateMs(identity.eightEnd);
102
+ else if (identity.overdate) vipExpire = parseDateMs(identity.overdate);
103
+ else if (vipData.mygreen) vipExpire = parseDateMs(vipData.mygreen);
104
+ else if (svip === 0 && Number(vipData.userinfo?.expire || 0) > 0) {
105
+ vipExpire = parseDateMs(vipData.userinfo.expire);
106
+ }
107
+
108
+ const isSvipExpired = svipExpire !== undefined && svipExpire <= now;
109
+ const isVipExpired = vipExpire !== undefined && vipExpire <= now;
110
+
48
111
  let vipStatus: "vip" | "nonVip" | "unknown" = "nonVip";
49
112
  let vipTier: "svip" | "vip" | undefined = undefined;
50
113
  let vipLevel: number | undefined = undefined;
51
114
  let vipYear: boolean | undefined = undefined;
52
115
  let vipExpireTime: number | undefined = undefined;
53
116
 
54
- if (svip > 0) {
117
+ // 1. 如果存在未过期的 SVIP
118
+ if (svip > 0 && !isSvipExpired) {
55
119
  vipStatus = "vip";
56
120
  vipTier = "svip";
57
121
  vipLevel = level > 0 ? Math.min(Math.max(level, 1), 10) : 1;
58
122
  vipYear = isYear;
59
- } else if (isVip > 0) {
123
+ vipExpireTime = svipExpire ?? (vipExpire && !isVipExpired ? vipExpire : undefined);
124
+ } else if (isVip > 0 && !isVipExpired) {
125
+ // 2. 如果存在普通 VIP(若 SVIP 已过期但 VIP 未过期,降级并展示未过期的 VIP 到期时间)
60
126
  vipStatus = "vip";
61
127
  vipTier = "vip";
62
128
  vipLevel = level > 0 ? Math.min(Math.max(level, 1), 10) : 1;
63
129
  vipYear = isYear;
130
+ vipExpireTime = vipExpire ?? (svipExpire && !isSvipExpired ? svipExpire : undefined);
64
131
  }
65
132
 
66
- // 若基本 identity 未命中 VIP,尝试从附加的 Info / NewIconInfo / Icons 解析
133
+ // 3. 若 identity 未命中有效 VIP(例如 SVIP 已过期,且普通 VIP 缺少基础标志或需通过勋章校准)
67
134
  if (vipStatus === "nonVip") {
68
135
  const info = vipData.Info || vipData.info || {};
69
136
  const iconList = info.NewIconInfo?.iconlist || vipData.NewIconInfo?.iconlist || [];
@@ -74,18 +141,19 @@ export function deriveVipInfo(body: any): NormalizedVipInfo {
74
141
  vipTier = iconVip.vipTier;
75
142
  vipLevel = iconVip.vipLevel;
76
143
  vipYear = iconVip.vipYear;
144
+ if (iconVip.vipTier === "vip" && vipExpire && vipExpire > now) {
145
+ vipExpireTime = vipExpire;
146
+ } else if (iconVip.vipTier === "svip" && svipExpire && svipExpire > now) {
147
+ vipExpireTime = svipExpire;
148
+ } else {
149
+ vipExpireTime = undefined;
150
+ }
77
151
  }
78
152
  }
79
153
 
80
- const expireTs = Number(vipData.userinfo?.expire || 0);
81
- if (expireTs > 0) {
82
- vipExpireTime = expireTs * 1000;
83
- } else if (identity.HugeVipEnd) {
84
- const parsed = Date.parse(identity.HugeVipEnd);
85
- if (!isNaN(parsed)) vipExpireTime = parsed;
86
- } else if (identity.twelveEnd) {
87
- const parsed = Date.parse(identity.twelveEnd);
88
- if (!isNaN(parsed)) vipExpireTime = parsed;
154
+ // 4. 严苛防线:任何已过期的到期时间绝对不允许外露
155
+ if (vipExpireTime !== undefined && vipExpireTime <= now) {
156
+ vipExpireTime = undefined;
89
157
  }
90
158
 
91
159
  return {
@@ -131,17 +199,21 @@ export default async function user_vip_info(
131
199
 
132
200
  let vip = deriveVipInfo(cgiRes.body);
133
201
 
134
- // 若 vip_login_base 无法推断出有效 VIP,自动通过主页头部接口获取勋章母版特征
135
- if (vip.vipStatus === "nonVip" && uin && uin !== "0") {
202
+ // 若 vip_login_base 无法推断出有效 VIP,或推断出的 SVIP 已经过期,或需要主页勋章校准,
203
+ // 自动通过主页头部接口获取勋章母版特征
204
+ if ((vip.vipStatus === "nonVip" || vip.vipTier === "svip") && uin && uin !== "0") {
136
205
  try {
137
206
  const detailRes = await user_detail_fn({ uin, cookie: query.cookie }, req);
138
207
  const detailData = detailRes.body?.data || {};
139
208
  const fallbackVip = deriveVipInfo(detailData);
140
209
  if (fallbackVip.vipStatus === "vip") {
141
- vip = {
142
- ...vip,
143
- ...fallbackVip,
144
- };
210
+ if (vip.vipStatus === "nonVip" || fallbackVip.vipTier === "vip" || !vip.vipExpireTime || vip.vipExpireTime <= Date.now()) {
211
+ vip = {
212
+ ...vip,
213
+ ...fallbackVip,
214
+ vipExpireTime: (vip.vipExpireTime && vip.vipExpireTime > Date.now()) ? vip.vipExpireTime : fallbackVip.vipExpireTime,
215
+ };
216
+ }
145
217
  }
146
218
  } catch {}
147
219
  }
@@ -156,3 +228,4 @@ export default async function user_vip_info(
156
228
  cookie: cgiRes.cookie,
157
229
  };
158
230
  }
231
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qqmusic-api-enhanced",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "QQ音乐增强版API,支持扫码登录/凭证刷新/直链提取/歌词解密/歌单歌手检索 || QQ Music Enhanced API for Node.js & Bun",
5
5
  "type": "module",
6
6
  "main": "main.js",