@sansenjian/qq-music-api 2.1.1 → 2.2.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.
Files changed (173) hide show
  1. package/.babelrc +4 -0
  2. package/.eslintrc.json +20 -19
  3. package/AGENTS.md +153 -0
  4. package/README.md +162 -136
  5. package/{app.js → app.ts} +24 -31
  6. package/config/user-info.ts +71 -0
  7. package/index.ts +1 -0
  8. package/{jest.config.js → jest.config.ts} +19 -7
  9. package/middlewares/koa-cors.ts +81 -0
  10. package/module/apis/UCommon/UCommon.ts +13 -0
  11. package/module/apis/album/getAlbumInfo.ts +22 -0
  12. package/module/apis/comments/getComments.ts +23 -0
  13. package/module/apis/digitalAlbum/getDigitalAlbumLists.ts +23 -0
  14. package/module/apis/downloadQQMusic.ts +51 -0
  15. package/module/apis/music/getLyric.ts +34 -0
  16. package/module/apis/mv/getMvByTag.ts +24 -0
  17. package/module/apis/radio/getRadioLists.ts +27 -0
  18. package/module/apis/rank/getTopLists.ts +37 -0
  19. package/module/apis/search/getHotKey.ts +24 -0
  20. package/module/apis/search/getSearchByKey.ts +31 -0
  21. package/module/apis/search/getSmartbox.ts +43 -0
  22. package/module/apis/singers/getSimilarSinger.ts +25 -0
  23. package/module/apis/singers/getSingerDesc.ts +25 -0
  24. package/module/apis/singers/getSingerMv.ts +24 -0
  25. package/module/apis/singers/getSingerStarNum.ts +24 -0
  26. package/module/apis/songLists/songListCategories.ts +22 -0
  27. package/module/apis/songLists/songListDetail.ts +27 -0
  28. package/module/apis/songLists/songLists.ts +35 -0
  29. package/module/apis/u_common.ts +29 -0
  30. package/module/apis/user/checkQQLoginQr.ts +230 -0
  31. package/module/apis/user/getQQLoginQr.ts +28 -0
  32. package/module/apis/user/getUserAvatar.ts +32 -0
  33. package/module/apis/user/getUserLikedSongs.ts +145 -0
  34. package/module/apis/user/getUserPlaylists.ts +163 -0
  35. package/module/apis/y_common.ts +44 -0
  36. package/module/config.ts +24 -0
  37. package/module/index.ts +95 -0
  38. package/package.json +25 -6
  39. package/pnpm-workspace.yaml +2 -0
  40. package/public/index.html +411 -29
  41. package/routers/context/batchGetSongInfo.ts +60 -0
  42. package/routers/context/batchGetSongLists.ts +46 -0
  43. package/routers/context/checkQQLoginQr.ts +19 -0
  44. package/routers/context/{cookies.js → cookies.ts} +14 -12
  45. package/routers/context/getAlbumInfo.ts +31 -0
  46. package/routers/context/getComments.ts +51 -0
  47. package/routers/context/getDigitalAlbumLists.ts +18 -0
  48. package/routers/context/getDownloadQQMusic.ts +17 -0
  49. package/routers/context/getHotkey.ts +25 -0
  50. package/routers/context/getImageUrl.ts +29 -0
  51. package/routers/context/getLyric.ts +32 -0
  52. package/routers/context/getMusicPlay.ts +102 -0
  53. package/routers/context/getMv.ts +61 -0
  54. package/routers/context/getMvByTag.ts +18 -0
  55. package/routers/context/getMvPlay.ts +114 -0
  56. package/routers/context/getNewDisks.ts +58 -0
  57. package/routers/context/getQQLoginQr.ts +16 -0
  58. package/routers/context/getRadioLists.ts +18 -0
  59. package/routers/context/getRanks.ts +67 -0
  60. package/routers/context/getRecommend.ts +92 -0
  61. package/routers/context/getSearchByKey.ts +34 -0
  62. package/routers/context/getSimilarSinger.ts +29 -0
  63. package/routers/context/getSingerAlbum.ts +58 -0
  64. package/routers/context/getSingerDesc.ts +30 -0
  65. package/routers/context/getSingerHotsong.ts +58 -0
  66. package/routers/context/getSingerList.ts +56 -0
  67. package/routers/context/getSingerMv.ts +41 -0
  68. package/routers/context/getSingerStarNum.ts +29 -0
  69. package/routers/context/getSmartbox.ts +27 -0
  70. package/routers/context/getSongInfo.ts +51 -0
  71. package/routers/context/{getSongListCategories.js → getSongListCategories.ts} +5 -4
  72. package/routers/context/getSongListDetail.ts +22 -0
  73. package/routers/context/getSongLists.ts +30 -0
  74. package/routers/context/getTicketInfo.ts +51 -0
  75. package/routers/context/getTopLists.ts +18 -0
  76. package/routers/context/getUserAvatar.ts +53 -0
  77. package/routers/context/getUserLikedSongs.ts +28 -0
  78. package/routers/context/getUserPlaylists.ts +29 -0
  79. package/routers/context/index.ts +87 -0
  80. package/routers/{router.js → router.ts} +7 -55
  81. package/routers/types.ts +18 -0
  82. package/routers/util.ts +231 -0
  83. package/tests/integration/api/api.test.ts +852 -0
  84. package/tests/integration/middleware/{cors.test.js → cors.test.ts} +7 -3
  85. package/tests/setup/jest.setup.ts +15 -0
  86. package/tests/setup/testUtils.ts +35 -0
  87. package/tests/unit/util/request.test.ts +177 -0
  88. package/tsconfig.json +20 -0
  89. package/tsconfig.test.json +8 -0
  90. package/types/api.ts +105 -0
  91. package/types/global.d.ts +26 -0
  92. package/types/index.d.ts +97 -0
  93. package/util/apiResponse.ts +97 -0
  94. package/util/colors.ts +31 -0
  95. package/util/cookie.ts +40 -0
  96. package/util/{loginUtils.js → loginUtils.ts} +3 -5
  97. package/util/lyricParse.ts +86 -0
  98. package/util/request.ts +141 -0
  99. package/.github/FUNDING.yml +0 -12
  100. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
  101. package/.github/ISSUE_TEMPLATE/custom.md +0 -24
  102. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  103. package/config/user-info.js +0 -42
  104. package/index.js +0 -1
  105. package/middlewares/koa-cors.js +0 -97
  106. package/module/apis/UCommon/UCommon.js +0 -6
  107. package/module/apis/album/getAlbumInfo.js +0 -33
  108. package/module/apis/comments/getComments.js +0 -35
  109. package/module/apis/digitalAlbum/getDigitalAlbumLists.js +0 -34
  110. package/module/apis/downloadQQMusic.js +0 -41
  111. package/module/apis/music/getLyric.js +0 -41
  112. package/module/apis/mv/getMvByTag.js +0 -35
  113. package/module/apis/radio/getRadioLists.js +0 -38
  114. package/module/apis/rank/getTopLists.js +0 -44
  115. package/module/apis/search/getHotKey.js +0 -35
  116. package/module/apis/search/getSearchByKey.js +0 -45
  117. package/module/apis/search/getSmartbox.js +0 -34
  118. package/module/apis/singers/getSimilarSinger.js +0 -36
  119. package/module/apis/singers/getSingerDesc.js +0 -37
  120. package/module/apis/singers/getSingerMv.js +0 -35
  121. package/module/apis/singers/getSingerStarNum.js +0 -35
  122. package/module/apis/songLists/songListCategories.js +0 -33
  123. package/module/apis/songLists/songListDetail.js +0 -38
  124. package/module/apis/songLists/songLists.js +0 -41
  125. package/module/apis/u_common.js +0 -17
  126. package/module/apis/user/checkQQLoginQr.js +0 -125
  127. package/module/apis/user/getQQLoginQr.js +0 -17
  128. package/module/apis/y_common.js +0 -27
  129. package/module/config.js +0 -31
  130. package/module/index.js +0 -82
  131. package/routers/context/batchGetSongInfo.js +0 -59
  132. package/routers/context/batchGetSongLists.js +0 -50
  133. package/routers/context/checkQQLoginQr.js +0 -17
  134. package/routers/context/getAlbumInfo.js +0 -27
  135. package/routers/context/getComments.js +0 -51
  136. package/routers/context/getDigitalAlbumLists.js +0 -14
  137. package/routers/context/getDownloadQQMusic.js +0 -14
  138. package/routers/context/getHotkey.js +0 -14
  139. package/routers/context/getImageUrl.js +0 -34
  140. package/routers/context/getLyric.js +0 -26
  141. package/routers/context/getMusicPlay.js +0 -116
  142. package/routers/context/getMv.js +0 -56
  143. package/routers/context/getMvByTag.js +0 -15
  144. package/routers/context/getMvPlay.js +0 -128
  145. package/routers/context/getNewDisks.js +0 -50
  146. package/routers/context/getQQLoginQr.js +0 -12
  147. package/routers/context/getRadioLists.js +0 -14
  148. package/routers/context/getRanks.js +0 -103
  149. package/routers/context/getRecommend.js +0 -86
  150. package/routers/context/getSearchByKey.js +0 -33
  151. package/routers/context/getSimilarSinger.js +0 -25
  152. package/routers/context/getSingerAlbum.js +0 -52
  153. package/routers/context/getSingerDesc.js +0 -25
  154. package/routers/context/getSingerHotsong.js +0 -52
  155. package/routers/context/getSingerList.js +0 -51
  156. package/routers/context/getSingerMv.js +0 -32
  157. package/routers/context/getSingerStarNum.js +0 -24
  158. package/routers/context/getSmartbox.js +0 -24
  159. package/routers/context/getSongInfo.js +0 -49
  160. package/routers/context/getSongListDetail.js +0 -25
  161. package/routers/context/getSongLists.js +0 -32
  162. package/routers/context/getTicketInfo.js +0 -45
  163. package/routers/context/getTopLists.js +0 -15
  164. package/routers/context/index.js +0 -74
  165. package/tests/integration/api/api.test.js +0 -116
  166. package/tests/setup/jest.setup.js +0 -46
  167. package/tests/setup/testUtils.js +0 -106
  168. package/tests/unit/util/lyricParse.test.js +0 -97
  169. package/tests/unit/util/request.test.js +0 -66
  170. package/util/colors.js +0 -16
  171. package/util/cookie.js +0 -22
  172. package/util/lyricParse.js +0 -67
  173. package/util/request.js +0 -57
@@ -0,0 +1,22 @@
1
+ import { handleApi } from '../../../util/apiResponse';
2
+ import y_common from '../y_common';
3
+ import type { ApiOptions } from '../../../types/api';
4
+
5
+ export default async ({ method = 'get', params = {}, option = {} }: ApiOptions) => {
6
+ const data = {
7
+ ...params,
8
+ format: 'json',
9
+ outCharset: 'utf-8'
10
+ };
11
+ const options = {
12
+ ...option,
13
+ params: data
14
+ };
15
+ return handleApi(
16
+ y_common({
17
+ url: '/splcloud/fcgi-bin/fcg_get_diss_tag_conf.fcg',
18
+ method: method as string,
19
+ options
20
+ })
21
+ );
22
+ };
@@ -0,0 +1,27 @@
1
+ import { handleApi } from '../../../util/apiResponse';
2
+ import y_common from '../y_common';
3
+ import type { ApiOptions } from '../../../types/api';
4
+
5
+ export default async ({ method = 'get', params = {}, option = {} }: ApiOptions) => {
6
+ const data = {
7
+ ...params,
8
+ format: 'json',
9
+ outCharset: 'utf-8',
10
+ type: 1,
11
+ json: 1,
12
+ utf8: 1,
13
+ onlysong: 0,
14
+ new_format: 1
15
+ };
16
+ const options = {
17
+ ...option,
18
+ params: data
19
+ };
20
+ return handleApi(
21
+ y_common({
22
+ url: '/qzone/fcg-bin/fcg_ucc_getcdinfo_byids_cp.fcg',
23
+ method: method as string,
24
+ options
25
+ })
26
+ );
27
+ };
@@ -0,0 +1,35 @@
1
+ import { handleApi } from '../../../util/apiResponse';
2
+ import y_common from '../y_common';
3
+ import type { ApiOptions } from '../../../types/api';
4
+
5
+ export default async ({ method = 'get', params = {}, option = {} }: ApiOptions) => {
6
+ const data = {
7
+ ...params,
8
+ format: 'json',
9
+ outCharset: 'utf-8',
10
+ picmid: 1
11
+ };
12
+ const options = {
13
+ ...option,
14
+ params: data
15
+ };
16
+ return handleApi(
17
+ y_common({
18
+ url: '/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg',
19
+ method: method as string,
20
+ options
21
+ }),
22
+ {
23
+ transformData: (response: unknown) => {
24
+ if (typeof response === 'string') {
25
+ const reg = /^\w+\(([^()]+)\)$/;
26
+ const matches = response.match(reg);
27
+ if (matches) {
28
+ return JSON.parse(matches[1]);
29
+ }
30
+ }
31
+ return response;
32
+ }
33
+ }
34
+ );
35
+ };
@@ -0,0 +1,29 @@
1
+ import { AxiosRequestConfig, Method } from 'axios';
2
+ import request from '../../util/request';
3
+ import * as config from '../config';
4
+
5
+ interface UCommonOptions {
6
+ options?: AxiosRequestConfig;
7
+ method?: Method | string;
8
+ }
9
+
10
+ export default ({ options = {}, method = 'get' }: UCommonOptions) => {
11
+ const opts: AxiosRequestConfig = { ...options };
12
+
13
+ // Merge commonParams into params for query string
14
+ opts.params = { ...config.commonParams, ...(opts.params || {}) };
15
+
16
+ opts.headers = {
17
+ referer: 'https://y.qq.com/portal/player.html',
18
+ host: 'u.y.qq.com',
19
+ 'content-type': 'application/x-www-form-urlencoded',
20
+ ...(opts.headers || {})
21
+ };
22
+
23
+ if (process.env.DEBUG === 'true') {
24
+ const logOpts = { ...opts, headers: { ...opts.headers, cookie: '[REDACTED]' } };
25
+ console.log('https://u.y.qq.com/cgi-bin/musicu.fcg', { opts: logOpts });
26
+ }
27
+
28
+ return request('https://u.y.qq.com/cgi-bin/musicu.fcg', method as Method, opts, 'u');
29
+ };
@@ -0,0 +1,230 @@
1
+ import type { ApiFunction, ApiOptions, ApiResponse } from '../../../types/api';
2
+ import { customResponse, errorResponse } from '../../../util/apiResponse';
3
+ import { getGtk, getGuid } from '../../../util/loginUtils';
4
+
5
+ interface LoginSession {
6
+ loginUin: string;
7
+ uin: string;
8
+ cookie: string;
9
+ cookieList: string[];
10
+ cookieObject: Record<string, string>;
11
+ }
12
+
13
+ const REQUEST_TIMEOUT_MS = 10000;
14
+ const DEBUG_ENABLED = process.env.DEBUG === 'true';
15
+
16
+ const debugLog = (message: string, payload?: unknown) => {
17
+ if (DEBUG_ENABLED) {
18
+ console.log(`[checkQQLoginQr] ${message}`, payload ?? '');
19
+ }
20
+ };
21
+
22
+ const parseSetCookie = (setCookieHeader: string | null): string[] => {
23
+ if (!setCookieHeader) return [];
24
+ const cookies: string[] = [];
25
+ const parts = setCookieHeader.split(/,(?=\s*[a-zA-Z_]+=)/);
26
+ for (const part of parts) {
27
+ const cookiePair = part.split(';')[0].trim();
28
+ if (cookiePair && cookiePair.includes('=') && cookiePair.split('=')[1]) {
29
+ cookies.push(cookiePair);
30
+ }
31
+ }
32
+ return cookies;
33
+ };
34
+
35
+ const fetchWithTimeout = async (input: string, init: RequestInit = {}, timeout = REQUEST_TIMEOUT_MS) => {
36
+ const controller = new AbortController();
37
+ const timer = setTimeout(() => controller.abort(), timeout);
38
+
39
+ try {
40
+ return await fetch(input, {
41
+ ...init,
42
+ signal: controller.signal
43
+ });
44
+ } finally {
45
+ clearTimeout(timer);
46
+ }
47
+ };
48
+
49
+ const buildLoginSession = (cookie: string): LoginSession => {
50
+ const cookieList = cookie
51
+ .split(';')
52
+ .map(item => item.trim())
53
+ .filter(Boolean);
54
+
55
+ const cookieObject: Record<string, string> = {};
56
+ cookieList.forEach(item => {
57
+ const [key, value = ''] = item.split('=');
58
+ if (key && value) {
59
+ cookieObject[key] = value;
60
+ }
61
+ });
62
+
63
+ const loginUin = cookieObject.uin || '';
64
+
65
+ return {
66
+ loginUin,
67
+ uin: loginUin,
68
+ cookie,
69
+ cookieList,
70
+ cookieObject
71
+ };
72
+ };
73
+
74
+ const checkQQLoginQr: ApiFunction = async ({ method = 'get', params = {}, option = {} }: ApiOptions): Promise<ApiResponse> => {
75
+ const { ptqrtoken, qrsig } = params;
76
+ if (!ptqrtoken || !qrsig) {
77
+ return errorResponse('参数错误', 400);
78
+ }
79
+
80
+ try {
81
+ const url = `https://ssl.ptlogin2.qq.com/ptqrlogin?u1=https%3A%2F%2Fgraph.qq.com%2Foauth2.0%2Flogin_jump&ptqrtoken=${ptqrtoken}&ptredirect=0&h=1&t=1&g=1&from_ui=1&ptlang=2052&action=0-0-1711022193435&js_ver=23111510&js_type=1&login_sig=du-YS1h8*0GqVqcrru0pXkpwVg2DYw-DtbFulJ62IgPf6vfiJe*4ONVrYc5hMUNE&pt_uistyle=40&aid=716027609&daid=383&pt_3rd_aid=100497308&&o1vId=3674fc47871e9c407d8838690b355408&pt_js_version=v1.48.1`;
82
+
83
+ const response = await fetchWithTimeout(url, { headers: { Cookie: `qrsig=${qrsig}` } });
84
+ const data = (await response.text()) || '';
85
+
86
+ const cookieMap = new Map<string, string>();
87
+ const setCookie = (setCookieHeader: string | null) => {
88
+ const cookies = parseSetCookie(setCookieHeader);
89
+ for (const cookie of cookies) {
90
+ const [name] = cookie.split('=');
91
+ cookieMap.set(name, cookie);
92
+ }
93
+ };
94
+
95
+ setCookie(response.headers.get('Set-Cookie'));
96
+
97
+ const refresh = data.includes('已失效');
98
+ if (!data.includes('登录成功')) {
99
+ return customResponse(
100
+ {
101
+ isOk: false,
102
+ refresh,
103
+ message: (refresh && '二维码已失效') || '未扫描二维码'
104
+ },
105
+ 200
106
+ );
107
+ }
108
+
109
+ const allCookie = () => Array.from(cookieMap.values());
110
+
111
+ const urlMatch = data.match(/(?:'((?:https?|ftp):\/\/[^\s/$.?#].[^\s]*)')/g);
112
+ if (!urlMatch || !urlMatch[0]) {
113
+ return errorResponse('Failed to extract checkSigUrl from response', 502);
114
+ }
115
+ const checkSigUrl = urlMatch[0].replace(/'/g, '');
116
+ const checkSigRes = await fetchWithTimeout(checkSigUrl, {
117
+ redirect: 'manual',
118
+ headers: { Cookie: allCookie().join('; ') }
119
+ });
120
+
121
+ const checkSigCookie = checkSigRes.headers.get('Set-Cookie');
122
+ const pSkeyMatch = checkSigCookie?.match(/p_skey=([^;]+)/);
123
+ if (!pSkeyMatch || !pSkeyMatch[1]) {
124
+ return errorResponse('Failed to extract p_skey from response', 502);
125
+ }
126
+ const p_skey = pSkeyMatch[1];
127
+ const gtk = getGtk(p_skey);
128
+ setCookie(checkSigCookie);
129
+
130
+ const authorizeUrl = 'https://graph.qq.com/oauth2.0/authorize';
131
+
132
+ const getAuthorizeData = (g_tk: number) => {
133
+ const data = new FormData();
134
+ data.append('response_type', 'code');
135
+ data.append('client_id', '100497308');
136
+ data.append('redirect_uri', 'https://y.qq.com/portal/wx_redirect.html?login_type=1&surl=https://y.qq.com/');
137
+ data.append('scope', 'get_user_info,get_app_friends');
138
+ data.append('state', 'state');
139
+ data.append('switch', '');
140
+ data.append('from_ptlogin', '1');
141
+ data.append('src', '1');
142
+ data.append('update_auth', '1');
143
+ data.append('openapi', '1010_1030');
144
+ data.append('g_tk', g_tk.toString());
145
+ data.append('auth_time', new Date().toString());
146
+ data.append('ui', getGuid());
147
+ return data;
148
+ };
149
+
150
+ const authorizeRes = await fetchWithTimeout(authorizeUrl, {
151
+ redirect: 'manual',
152
+ method: 'POST',
153
+ body: getAuthorizeData(gtk),
154
+ headers: {
155
+ Cookie: allCookie().join('; ')
156
+ }
157
+ });
158
+ setCookie(authorizeRes.headers.get('Set-Cookie'));
159
+
160
+ const location = authorizeRes.headers.get('Location');
161
+ const contentType = authorizeRes.headers.get('Content-Type');
162
+
163
+ debugLog('authorize response meta', {
164
+ status: authorizeRes.status,
165
+ redirected: authorizeRes.redirected,
166
+ hasLocation: Boolean(location),
167
+ contentType
168
+ });
169
+
170
+ if (authorizeRes.status < 300 || authorizeRes.status >= 400 || !location) {
171
+ return errorResponse('授权响应异常,未返回跳转地址', 502);
172
+ }
173
+
174
+ const codeMatch = location.match(/[?&]code=([^&]+)/);
175
+ if (!codeMatch || !codeMatch[1]) {
176
+ debugLog('authorize location parse failed', { location });
177
+ return errorResponse('授权响应中未找到 code 参数', 502);
178
+ }
179
+ const code = codeMatch[1];
180
+
181
+ const getFcgReqData = (g_tk: number, code: string) => {
182
+ const data = {
183
+ comm: {
184
+ g_tk,
185
+ platform: 'yqq',
186
+ ct: 24,
187
+ cv: 0
188
+ },
189
+ req: {
190
+ module: 'QQConnectLogin.LoginServer',
191
+ method: 'QQLogin',
192
+ param: {
193
+ code
194
+ }
195
+ }
196
+ };
197
+ return JSON.stringify(data);
198
+ };
199
+
200
+ const loginUrl = 'https://u.y.qq.com/cgi-bin/musicu.fcg';
201
+ const loginRes = await fetchWithTimeout(loginUrl, {
202
+ method: 'POST',
203
+ body: getFcgReqData(gtk, code),
204
+ headers: {
205
+ 'Content-Type': 'application/x-www-form-urlencoded',
206
+ Cookie: allCookie().join('; ')
207
+ }
208
+ });
209
+ setCookie(loginRes.headers.get('Set-Cookie'));
210
+
211
+ const sessionCookie = allCookie().join('; ');
212
+
213
+ return customResponse(
214
+ {
215
+ isOk: true,
216
+ message: '登录成功',
217
+ session: buildLoginSession(sessionCookie)
218
+ },
219
+ 200
220
+ );
221
+ } catch (error) {
222
+ if ((error as Error).name === 'AbortError') {
223
+ return errorResponse('登录检查超时', 504);
224
+ }
225
+
226
+ return errorResponse('登录检查失败', 502);
227
+ }
228
+ };
229
+
230
+ export default checkQQLoginQr;
@@ -0,0 +1,28 @@
1
+ import type { ApiFunction, ApiOptions } from '../../../types/api';
2
+ import { customResponse, errorResponse } from '../../../util/apiResponse';
3
+ import { hash33 } from '../../../util/loginUtils';
4
+
5
+ const getQQLoginQr: ApiFunction = async ({ method = 'get', params = {}, option = {} }: ApiOptions) => {
6
+ try {
7
+ const url =
8
+ 'https://ssl.ptlogin2.qq.com/ptqrshow?appid=716027609&e=2&l=M&s=3&d=72&v=4&t=0.9698127522807933&daid=383&pt_3rd_aid=100497308&u1=https%3A%2F%2Fgraph.qq.com%2Foauth2.0%2Flogin_jump';
9
+
10
+ const response = await fetch(url);
11
+ const data = await response.arrayBuffer();
12
+ const img = 'data:image/png;base64,' + (data && Buffer.from(data).toString('base64'));
13
+ const cookieHeader = response.headers.get('Set-Cookie');
14
+ const match = cookieHeader?.match(/qrsig=([^;]+)/);
15
+
16
+ if (!match) {
17
+ return errorResponse('Failed to get qrsig from response', 502);
18
+ }
19
+
20
+ const qrsig = match[1];
21
+
22
+ return customResponse({ img, ptqrtoken: hash33(qrsig), qrsig }, 200);
23
+ } catch (error) {
24
+ return errorResponse('Failed to fetch QQ login QR', 502);
25
+ }
26
+ };
27
+
28
+ export default getQQLoginQr;
@@ -0,0 +1,32 @@
1
+ import request from '../../../util/request';
2
+
3
+ // 获取 QQ 用户头像
4
+ export const getUserAvatar = async (params: {
5
+ k?: string;
6
+ uin?: string;
7
+ size?: number;
8
+ }) => {
9
+ const { k, uin, size = 140 } = params;
10
+
11
+ // 如果提供了 k 参数,使用 k 参数获取头像
12
+ if (k) {
13
+ const url = `https://thirdqq.qlogo.cn/g?b=sdk&k=${k}&s=${size}`;
14
+
15
+ return {
16
+ avatarUrl: url,
17
+ message: '头像 URL 获取成功'
18
+ };
19
+ }
20
+
21
+ // 如果提供了 uin,尝试从 uin 获取头像
22
+ if (uin) {
23
+ const url = `https://q.qlogo.cn/headimg_dl?dst_uin=${uin}&spec=${size}`;
24
+
25
+ return {
26
+ avatarUrl: url,
27
+ message: '头像 URL 获取成功'
28
+ };
29
+ }
30
+
31
+ throw new Error('缺少 k 或 uin 参数');
32
+ };
@@ -0,0 +1,145 @@
1
+ import request from '../../../util/request';
2
+ import { customResponse, errorResponse } from '../../../util/apiResponse';
3
+ import type { ApiResponse } from '../../../types/api';
4
+
5
+ interface LikedSong {
6
+ [key: string]: unknown;
7
+ }
8
+
9
+ const DEBUG_ENABLED = process.env.DEBUG === 'true';
10
+
11
+ const debugLog = (message: string, payload?: unknown) => {
12
+ if (DEBUG_ENABLED) {
13
+ console.log(`[getUserLikedSongs] ${message}`, payload ?? '');
14
+ }
15
+ };
16
+
17
+ // 获取用户喜欢的歌曲列表
18
+ // 注意:此接口需要有效的 QQ 音乐 Cookie 才能正常工作
19
+ export const getUserLikedSongs = async (params: {
20
+ uin: string;
21
+ offset?: number;
22
+ limit?: number;
23
+ }): Promise<ApiResponse> => {
24
+ const { uin, offset = 0, limit = 30 } = params;
25
+ const page = Math.floor(offset / limit) + 1;
26
+
27
+ // 使用 c6.y.qq.com/rsc/fcgi-bin/fcg_get_profile_homepage.fcg 接口
28
+ // 通过 mymusic 字段获取用户喜欢的歌曲信息
29
+ const url = 'https://c6.y.qq.com/rsc/fcgi-bin/fcg_get_profile_homepage.fcg';
30
+
31
+ try {
32
+ debugLog('request meta', {
33
+ url,
34
+ uin,
35
+ offset,
36
+ limit,
37
+ page,
38
+ hasGlobalCookie: Boolean(global.userInfo?.cookie),
39
+ cookieLength: global.userInfo?.cookie?.length || 0
40
+ });
41
+
42
+ const response = await request<Record<string, any>>({
43
+ url,
44
+ method: 'GET',
45
+ isUUrl: 'u',
46
+ options: {
47
+ params: {
48
+ _: Date.now(),
49
+ cv: 4747474,
50
+ ct: 24,
51
+ format: 'json',
52
+ inCharset: 'utf-8',
53
+ outCharset: 'utf-8',
54
+ notice: 0,
55
+ platform: 'yqq.json',
56
+ needNewCode: 0,
57
+ uin: Number.parseInt(uin, 10),
58
+ g_tk_new_20200303: 0,
59
+ g_tk: 0,
60
+ cid: 205360838,
61
+ userid: Number.parseInt(uin, 10),
62
+ reqfrom: 1,
63
+ reqtype: 0,
64
+ hostUin: 0,
65
+ loginUin: Number.parseInt(uin, 10)
66
+ },
67
+ headers: {
68
+ Referer: `https://y.qq.com/portal/profile.html?uin=${uin}`,
69
+ Cookie: global.userInfo?.cookie || ''
70
+ }
71
+ }
72
+ });
73
+
74
+ const payload = response.data;
75
+
76
+ debugLog('upstream payload summary', {
77
+ topLevelKeys: payload && typeof payload === 'object' ? Object.keys(payload) : null,
78
+ code: payload?.code,
79
+ hasData: Boolean(payload?.data),
80
+ dataKeys: payload?.data && typeof payload.data === 'object' ? Object.keys(payload.data) : []
81
+ });
82
+
83
+ if (!payload || typeof payload !== 'object') {
84
+ debugLog('invalid payload received', payload);
85
+ return errorResponse('用户喜欢的歌曲响应格式无效', 502);
86
+ }
87
+
88
+ if (typeof payload.code === 'number' && payload.code !== 0) {
89
+ debugLog('upstream business error payload', payload);
90
+ return errorResponse(payload.msg || payload.message || '获取用户喜欢的歌曲失败', 502);
91
+ }
92
+
93
+ // 从 mymusic 字段中提取喜欢的歌曲信息
94
+ // mymusic 数组中的第一个元素通常包含"我喜欢"歌单的信息
95
+ const mymusic = payload?.data?.mymusic;
96
+ let likedSongsInfo = null;
97
+
98
+ if (Array.isArray(mymusic)) {
99
+ // 查找"我喜欢"歌单(通常 title 包含"喜欢"或 type 为 1)
100
+ likedSongsInfo = mymusic.find((item: any) => {
101
+ return item?.title && (item.title.includes('喜欢') || item.type === 1);
102
+ });
103
+ }
104
+
105
+ debugLog('liked songs info', likedSongsInfo);
106
+
107
+ if (!likedSongsInfo) {
108
+ debugLog('no liked songs info found in mymusic');
109
+ return customResponse({
110
+ response: {
111
+ code: 0,
112
+ data: {
113
+ songs: [],
114
+ total: 0,
115
+ hasMore: false
116
+ }
117
+ }
118
+ }, 200);
119
+ }
120
+
121
+ // 返回喜欢的歌曲信息(包含歌单 ID 和统计信息)
122
+ // 注意:完整的歌曲列表需要通过 musics.fcg 接口获取(需要二进制加密)
123
+ // 这里先返回歌单基本信息
124
+ return customResponse({
125
+ response: {
126
+ code: 0,
127
+ data: {
128
+ songs: [likedSongsInfo],
129
+ total: likedSongsInfo.num0 || 0,
130
+ hasMore: false,
131
+ info: {
132
+ title: likedSongsInfo.title,
133
+ songCount: likedSongsInfo.num0,
134
+ albumCount: likedSongsInfo.num1,
135
+ playlistCount: likedSongsInfo.num2,
136
+ id: likedSongsInfo.id
137
+ }
138
+ }
139
+ }
140
+ }, 200);
141
+ } catch (error) {
142
+ console.error('获取用户喜欢的歌曲失败:', error);
143
+ return errorResponse((error as Error).message || '获取用户喜欢的歌曲失败', 502);
144
+ }
145
+ };