@sansenjian/qq-music-api 2.2.7 → 2.2.9

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 (116) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +2 -0
  3. package/dist/app.js +24 -5
  4. package/dist/config/service-config.js +37 -0
  5. package/dist/jest.config.js +14 -2
  6. package/dist/middlewares/fallback-middleware.js +29 -0
  7. package/dist/module/apis/music/getMusicPlay.js +187 -0
  8. package/dist/module/apis/u_common.js +8 -2
  9. package/dist/module/apis/user/getUserPlaylists.js +5 -5
  10. package/dist/module/index.js +3 -1
  11. package/dist/package.json +4 -1
  12. package/dist/routers/context/batchGetSongInfo.js +11 -16
  13. package/dist/routers/context/batchGetSongLists.js +18 -20
  14. package/dist/routers/context/getAlbumInfo.js +10 -17
  15. package/dist/routers/context/getComments.js +12 -19
  16. package/dist/routers/context/getDailyRecommend.js +5 -17
  17. package/dist/routers/context/getHotkey.js +7 -8
  18. package/dist/routers/context/getMusicPlay.js +23 -81
  19. package/dist/routers/context/getMv.js +16 -22
  20. package/dist/routers/context/getMvPlay.js +48 -49
  21. package/dist/routers/context/getPersonalRecommend.js +13 -25
  22. package/dist/routers/context/getPlaylistTags.js +20 -26
  23. package/dist/routers/context/getRanks.js +9 -16
  24. package/dist/routers/context/getSingerAlbum.js +16 -22
  25. package/dist/routers/context/getSingerHotsong.js +16 -22
  26. package/dist/routers/context/getSingerList.js +9 -21
  27. package/dist/routers/context/getSongInfo.js +9 -16
  28. package/dist/routers/context/getSongListCategories.js +6 -7
  29. package/dist/routers/context/getSongListDetail.js +6 -8
  30. package/dist/routers/context/getUserAvatar.js +20 -33
  31. package/dist/routers/context/getUserPlaylists.js +6 -3
  32. package/dist/routers/context/index.js +94 -103
  33. package/dist/routers/util.js +31 -0
  34. package/dist/scripts/run-tests-with-flags.js +139 -0
  35. package/dist/util/cookie.js +15 -7
  36. package/dist/util/cookieResolver.js +66 -0
  37. package/dist/util/request.js +15 -6
  38. package/docs-dist/404.html +2 -2
  39. package/docs-dist/CHANGELOG-ARCHITECTURE.html +6 -6
  40. package/docs-dist/COOKIE_CONFIG_GUIDE.html +6 -6
  41. package/docs-dist/FALLBACK_MODE_GUIDE.html +101 -0
  42. package/docs-dist/README.html +6 -6
  43. package/docs-dist/TEST_USER_PLAYLISTS.html +6 -6
  44. package/docs-dist/USER_AVATAR_GUIDE.html +6 -6
  45. package/docs-dist/api/comments.html +6 -6
  46. package/docs-dist/api/index.html +6 -6
  47. package/docs-dist/api/music.html +6 -6
  48. package/docs-dist/api/other.html +6 -6
  49. package/docs-dist/api/playlist.html +6 -6
  50. package/docs-dist/api/rank.html +6 -6
  51. package/docs-dist/api/search.html +6 -6
  52. package/docs-dist/api/singer.html +6 -6
  53. package/docs-dist/api/user.html +6 -6
  54. package/docs-dist/assets/{CHANGELOG-ARCHITECTURE.md.BOe0ZtyR.js → CHANGELOG-ARCHITECTURE.md.r40JGJZK.js} +1 -1
  55. package/docs-dist/assets/{CHANGELOG-ARCHITECTURE.md.BOe0ZtyR.lean.js → CHANGELOG-ARCHITECTURE.md.r40JGJZK.lean.js} +1 -1
  56. package/docs-dist/assets/{COOKIE_CONFIG_GUIDE.md.D68AwXR2.js → COOKIE_CONFIG_GUIDE.md.BVXl7WHu.js} +1 -1
  57. package/docs-dist/assets/{COOKIE_CONFIG_GUIDE.md.D68AwXR2.lean.js → COOKIE_CONFIG_GUIDE.md.BVXl7WHu.lean.js} +1 -1
  58. package/docs-dist/assets/FALLBACK_MODE_GUIDE.md.BBdcIdh_.js +75 -0
  59. package/docs-dist/assets/FALLBACK_MODE_GUIDE.md.BBdcIdh_.lean.js +1 -0
  60. package/docs-dist/assets/{README.md.ZJQGJ1Gb.js → README.md.D6Tw0nRd.js} +1 -1
  61. package/docs-dist/assets/{README.md.ZJQGJ1Gb.lean.js → README.md.D6Tw0nRd.lean.js} +1 -1
  62. package/docs-dist/assets/{TEST_USER_PLAYLISTS.md.C02575X2.js → TEST_USER_PLAYLISTS.md.DSt20Igj.js} +1 -1
  63. package/docs-dist/assets/{TEST_USER_PLAYLISTS.md.C02575X2.lean.js → TEST_USER_PLAYLISTS.md.DSt20Igj.lean.js} +1 -1
  64. package/docs-dist/assets/{USER_AVATAR_GUIDE.md.BOqjn5Cm.js → USER_AVATAR_GUIDE.md.CVHPs2Dn.js} +1 -1
  65. package/docs-dist/assets/{USER_AVATAR_GUIDE.md.BOqjn5Cm.lean.js → USER_AVATAR_GUIDE.md.CVHPs2Dn.lean.js} +1 -1
  66. package/docs-dist/assets/{api_comments.md.DADvndEA.js → api_comments.md.79Q_C8Qp.js} +1 -1
  67. package/docs-dist/assets/{api_comments.md.DADvndEA.lean.js → api_comments.md.79Q_C8Qp.lean.js} +1 -1
  68. package/docs-dist/assets/{api_index.md.D5IASxxG.js → api_index.md.CU3By8tw.js} +1 -1
  69. package/docs-dist/assets/{api_index.md.D5IASxxG.lean.js → api_index.md.CU3By8tw.lean.js} +1 -1
  70. package/docs-dist/assets/{api_music.md.BgB8NmZq.js → api_music.md.B1AzLePX.js} +1 -1
  71. package/docs-dist/assets/{api_music.md.BgB8NmZq.lean.js → api_music.md.B1AzLePX.lean.js} +1 -1
  72. package/docs-dist/assets/{api_other.md.BkRWXX2z.js → api_other.md.DCg4bzA7.js} +1 -1
  73. package/docs-dist/assets/{api_other.md.BkRWXX2z.lean.js → api_other.md.DCg4bzA7.lean.js} +1 -1
  74. package/docs-dist/assets/{api_playlist.md.Dc0hTrZ4.js → api_playlist.md.8ACJ3QqD.js} +1 -1
  75. package/docs-dist/assets/{api_playlist.md.Dc0hTrZ4.lean.js → api_playlist.md.8ACJ3QqD.lean.js} +1 -1
  76. package/docs-dist/assets/{api_rank.md.DRisCFyT.js → api_rank.md.B8IP2ZRy.js} +1 -1
  77. package/docs-dist/assets/{api_rank.md.DRisCFyT.lean.js → api_rank.md.B8IP2ZRy.lean.js} +1 -1
  78. package/docs-dist/assets/{api_search.md.DNnMUZK0.js → api_search.md.DO9J6nvp.js} +1 -1
  79. package/docs-dist/assets/{api_search.md.DNnMUZK0.lean.js → api_search.md.DO9J6nvp.lean.js} +1 -1
  80. package/docs-dist/assets/{api_singer.md.DCmuxQkk.js → api_singer.md.CcL32xuN.js} +1 -1
  81. package/docs-dist/assets/{api_singer.md.DCmuxQkk.lean.js → api_singer.md.CcL32xuN.lean.js} +1 -1
  82. package/docs-dist/assets/{api_user.md.Cjm9GG3z.js → api_user.md.Cb7Ky3Sn.js} +1 -1
  83. package/docs-dist/assets/{api_user.md.Cjm9GG3z.lean.js → api_user.md.Cb7Ky3Sn.lean.js} +1 -1
  84. package/docs-dist/assets/{app.Dx_1wB58.js → app.CSainqD9.js} +1 -1
  85. package/docs-dist/assets/chunks/@localSearchIndexroot.BKleDIv-.js +1 -0
  86. package/docs-dist/assets/chunks/{VPLocalSearchBox.DwKWtsdX.js → VPLocalSearchBox.BUBaq7tw.js} +1 -1
  87. package/docs-dist/assets/chunks/framework.aJbMEiY9.js +19 -0
  88. package/docs-dist/assets/chunks/{theme.pGVgJ9Cx.js → theme.CzMhU0Ps.js} +2 -2
  89. package/docs-dist/assets/{guide_architecture.md.DGtNyuMH.js → guide_architecture.md.CzgqynmB.js} +1 -1
  90. package/docs-dist/assets/{guide_architecture.md.DGtNyuMH.lean.js → guide_architecture.md.CzgqynmB.lean.js} +1 -1
  91. package/docs-dist/assets/{guide_authentication.md.mtI5LfCw.js → guide_authentication.md.a8yTA8Xe.js} +1 -1
  92. package/docs-dist/assets/{guide_authentication.md.mtI5LfCw.lean.js → guide_authentication.md.a8yTA8Xe.lean.js} +1 -1
  93. package/docs-dist/assets/{guide_index.md.B-0SG46T.js → guide_index.md.BgUUL6fI.js} +1 -1
  94. package/docs-dist/assets/{guide_index.md.B-0SG46T.lean.js → guide_index.md.BgUUL6fI.lean.js} +1 -1
  95. package/docs-dist/assets/{guide_installation.md.k-KpAfxv.js → guide_installation.md.BCZ4jBl_.js} +1 -1
  96. package/docs-dist/assets/guide_installation.md.BCZ4jBl_.lean.js +1 -0
  97. package/docs-dist/assets/{guide_quickstart.md.Bff_KFOD.js → guide_quickstart.md.9-4dA6wS.js} +1 -1
  98. package/docs-dist/assets/guide_quickstart.md.9-4dA6wS.lean.js +1 -0
  99. package/docs-dist/assets/{index.md.xrs-uIyo.js → index.md.z0hAJioN.js} +1 -1
  100. package/docs-dist/assets/{index.md.xrs-uIyo.lean.js → index.md.z0hAJioN.lean.js} +1 -1
  101. package/docs-dist/assets/{reference_response-format.md.DKYTK6uJ.js → reference_response-format.md.VvQTLDZr.js} +1 -1
  102. package/docs-dist/assets/{reference_response-format.md.DKYTK6uJ.lean.js → reference_response-format.md.VvQTLDZr.lean.js} +1 -1
  103. package/docs-dist/guide/architecture.html +6 -6
  104. package/docs-dist/guide/authentication.html +6 -6
  105. package/docs-dist/guide/index.html +6 -6
  106. package/docs-dist/guide/installation.html +6 -6
  107. package/docs-dist/guide/quickstart.html +6 -6
  108. package/docs-dist/hashmap.json +1 -1
  109. package/docs-dist/index.html +5 -5
  110. package/docs-dist/reference/response-format.html +6 -6
  111. package/docs-dist/version.json +3 -3
  112. package/package.json +4 -1
  113. package/docs-dist/assets/chunks/@localSearchIndexroot.CMY5EIwU.js +0 -1
  114. package/docs-dist/assets/chunks/framework.o40iizuP.js +0 -19
  115. package/docs-dist/assets/guide_installation.md.k-KpAfxv.lean.js +0 -1
  116. package/docs-dist/assets/guide_quickstart.md.Bff_KFOD.lean.js +0 -1
package/CHANGELOG.md CHANGED
@@ -1,4 +1,17 @@
1
- ## 2.2.7 (2026-03-08)
1
+ ## 2.2.9 (2026-03-14)
2
+
3
+
4
+
5
+ ## 2.2.9 (2026-03-14)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * rewrite getMusicPlay url logic ([#12](https://github.com/sansenjian/qq-music-api/issues/12)) ([ec9823b](https://github.com/sansenjian/qq-music-api/commit/ec9823be7821f6c83427f1aada8848729936e1d2))
11
+
12
+
13
+
14
+ ## 2.2.8 (2026-03-08)
2
15
 
3
16
 
4
17
 
package/README.md CHANGED
@@ -9,6 +9,8 @@
9
9
  ![node](https://img.shields.io/badge/node-%3E%3D20.0.0-green?style=flat-square)
10
10
  <br />
11
11
  ![GitHub repo size](https://img.shields.io/github/repo-size/sansenjian/qq-music-api?style=flat-square) ![GitHub package.json version](https://img.shields.io/github/package-json/v/sansenjian/qq-music-api?style=flat-square) ![GitHub](https://img.shields.io/github/license/sansenjian/qq-music-api?style=flat-square) ![GitHub open issues](https://img.shields.io/github/issues/sansenjian/qq-music-api?style=flat-square) ![GitHub closed issues](https://img.shields.io/github/issues-closed/sansenjian/qq-music-api) ![GitHub last commit](https://img.shields.io/github/last-commit/sansenjian/qq-music-api?style=flat-square) ![GitHub top language](https://img.shields.io/github/languages/top/sansenjian/qq-music-api?style=flat-square)
12
+ <br />
13
+ [![Codecov](https://img.shields.io/codecov/c/github/sansenjian/qq-music-api?style=flat-square&logo=codecov)](https://codecov.io/gh/sansenjian/qq-music-api)
12
14
 
13
15
  </div>
14
16
 
package/dist/app.js CHANGED
@@ -12,20 +12,39 @@ const chalk_1 = __importDefault(require("chalk"));
12
12
  const koa_cors_1 = __importDefault(require("./middlewares/koa-cors"));
13
13
  const router_1 = __importDefault(require("./routers/router"));
14
14
  const cookie_1 = __importDefault(require("./util/cookie"));
15
+ const fallback_middleware_1 = __importDefault(require("./middlewares/fallback-middleware"));
15
16
  const colors_1 = __importDefault(require("./util/colors"));
16
17
  const user_info_1 = __importDefault(require("./config/user-info"));
18
+ const service_config_1 = __importDefault(require("./config/service-config"));
17
19
  const package_json_1 = __importDefault(require("./package.json"));
18
20
  const app = new koa_1.default();
19
21
  global.userInfo = user_info_1.default;
20
- console.log(chalk_1.default.green('\n🥳🎉 We had supported config the user cookies. \n'));
22
+ // 输出服务配置信息
23
+ console.log(chalk_1.default.green('\n🎵 QQ Music API Service Starting...\n'));
21
24
  console.log(colors_1.default.info(`Current Version: ${package_json_1.default.version}`));
22
- if (!(global.userInfo.loginUin || global.userInfo.uin)) {
23
- console.log(chalk_1.default.yellow(`😔 The configuration ${chalk_1.default.red('loginUin')} or your ${chalk_1.default.red('cookie')} in file ${chalk_1.default.green('config/user-info')} has not configured. \n`));
25
+ console.log(colors_1.default.info(`Fallback Mode: ${service_config_1.default.fallbackMode ? 'Enabled' : 'Disabled'}`));
26
+ console.log(colors_1.default.info(`Use Global Cookie: ${service_config_1.default.useGlobalCookie ? 'Yes' : 'No'}`));
27
+ if (service_config_1.default.fallbackMode) {
28
+ console.log(chalk_1.default.green('\n✅ 降级模式已启用:支持手动传递 Cookie\n'));
29
+ console.log('使用方式:');
30
+ console.log(' 1. Query 参数:GET /api/endpoint?cookie=your_cookie');
31
+ console.log(' 2. Header: X-Custom-Cookie: your_cookie');
32
+ console.log(' 3. Header: Cookie: your_cookie\n');
24
33
  }
25
- if (!global.userInfo.cookie) {
26
- console.log(chalk_1.default.yellow(`😔 The configuration ${chalk_1.default.red('cookie')} in file ${chalk_1.default.green('config/user-info')} has not configured. \n`));
34
+ if (!service_config_1.default.useGlobalCookie) {
35
+ console.log(chalk_1.default.yellow('\n⚠️ 全局 Cookie 未启用:需要登录的接口请手动传递 Cookie\n'));
36
+ }
37
+ if (service_config_1.default.useGlobalCookie) {
38
+ console.log(chalk_1.default.green('\n✅ 全局 Cookie 已启用\n'));
39
+ if (!(global.userInfo.loginUin || global.userInfo.uin)) {
40
+ console.log(chalk_1.default.yellow(`😔 The configuration ${chalk_1.default.red('loginUin')} or your ${chalk_1.default.red('cookie')} in file ${chalk_1.default.green('config/user-info')} has not configured. \n`));
41
+ }
42
+ if (!global.userInfo.cookie) {
43
+ console.log(chalk_1.default.yellow(`😔 The configuration ${chalk_1.default.red('cookie')} in file ${chalk_1.default.green('config/user-info')} has not configured. \n`));
44
+ }
27
45
  }
28
46
  app.use((0, koa_bodyparser_1.default)());
47
+ app.use((0, fallback_middleware_1.default)());
29
48
  app.use((0, cookie_1.default)());
30
49
  app.use((0, koa_static_1.default)(path_1.default.join(__dirname, 'public')));
31
50
  // logger
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const configPath = path_1.default.join(__dirname, './service-config.json');
9
+ // 创建默认配置
10
+ const defaultConfig = {
11
+ fallbackMode: true,
12
+ useGlobalCookie: false,
13
+ cookieParamName: 'cookie'
14
+ };
15
+ // 读取或创建配置文件
16
+ let config = defaultConfig;
17
+ if (fs_1.default.existsSync(configPath)) {
18
+ try {
19
+ const content = fs_1.default.readFileSync(configPath, 'utf-8');
20
+ config = { ...defaultConfig, ...JSON.parse(content) };
21
+ }
22
+ catch (e) {
23
+ console.warn('service-config.json 读取失败,使用默认配置');
24
+ fs_1.default.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf-8');
25
+ }
26
+ }
27
+ else {
28
+ fs_1.default.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf-8');
29
+ }
30
+ // 支持环境变量覆盖
31
+ if (process.env.FALLBACK_MODE === 'true') {
32
+ config.fallbackMode = true;
33
+ }
34
+ if (process.env.USE_GLOBAL_COOKIE === 'true') {
35
+ config.useGlobalCookie = true;
36
+ }
37
+ exports.default = config;
@@ -8,12 +8,22 @@ const config = {
8
8
  'module/**/*.ts',
9
9
  'routers/**/*.ts',
10
10
  'util/**/*.ts',
11
+ 'middlewares/**/*.ts',
11
12
  '!**/node_modules/**',
12
13
  '!**/tests/**',
13
- '!**/dist/**'
14
+ '!**/dist/**',
15
+ '!**/*.d.ts',
16
+ '!**/types/**/*.ts'
14
17
  ],
15
18
  coverageDirectory: 'coverage',
16
- coverageReporters: ['text', 'lcov', 'html'],
19
+ coverageReporters: ['text', 'lcov', 'html', 'json-summary', 'json'],
20
+ coveragePathIgnorePatterns: [
21
+ '/node_modules/',
22
+ '/tests/',
23
+ '/dist/',
24
+ '\\.d\\.ts$',
25
+ '/types/'
26
+ ],
17
27
  coverageThreshold: {
18
28
  global: {
19
29
  branches: 35,
@@ -26,6 +36,8 @@ const config = {
26
36
  modulePathIgnorePatterns: ['<rootDir>/dist/'],
27
37
  testTimeout: 10000,
28
38
  verbose: true,
39
+ collectCoverage: true, // 默认开启覆盖率
40
+ coverageProvider: 'v8', // 使用 v8 引擎,更准确
29
41
  moduleNameMapper: {
30
42
  '^@module/(.*)$': '<rootDir>/module/$1',
31
43
  '^@routers/(.*)$': '<rootDir>/routers/$1',
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const service_config_1 = __importDefault(require("../config/service-config"));
7
+ const cookieResolver_1 = require("../util/cookieResolver");
8
+ /**
9
+ * 降级模式中间件:支持通过 query/header 手动传递 Cookie。
10
+ */
11
+ const fallbackMiddleware = () => async (ctx, next) => {
12
+ if (!service_config_1.default.fallbackMode) {
13
+ await next();
14
+ return;
15
+ }
16
+ const { cookie, source } = (0, cookieResolver_1.resolveRequestCookie)(ctx, {
17
+ fallbackMode: true,
18
+ useGlobalCookie: false,
19
+ cookieParamName: service_config_1.default.cookieParamName
20
+ });
21
+ if (cookie) {
22
+ (0, cookieResolver_1.setRequestCookieContext)(ctx, cookie);
23
+ if (process.env.DEBUG === 'true') {
24
+ console.log(`[FallbackMode] use cookie from ${source}, length: ${cookie.length}`);
25
+ }
26
+ }
27
+ await next();
28
+ };
29
+ exports.default = fallbackMiddleware;
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const UCommon_1 = __importDefault(require("../UCommon/UCommon"));
7
+ const config_1 = require("../../config");
8
+ const cookieResolver_1 = require("../../../util/cookieResolver");
9
+ const DEFAULT_QUALITY = '128';
10
+ const FILE_TYPE_MAP = {
11
+ m4a: { prefix: 'C400', suffix: '.m4a' },
12
+ 128: { prefix: 'M500', suffix: '.mp3' },
13
+ 320: { prefix: 'M800', suffix: '.mp3' },
14
+ ape: { prefix: 'A000', suffix: '.ape' },
15
+ flac: { prefix: 'F000', suffix: '.flac' }
16
+ };
17
+ const normalizeFirstValue = (value) => {
18
+ if (Array.isArray(value)) {
19
+ return normalizeFirstValue(value[0]);
20
+ }
21
+ if (typeof value !== 'string') {
22
+ return undefined;
23
+ }
24
+ const normalized = value.trim();
25
+ return normalized || undefined;
26
+ };
27
+ const normalizeSongmidList = (songmid) => {
28
+ const values = Array.isArray(songmid) ? songmid : [songmid];
29
+ return values
30
+ .flatMap(value => {
31
+ if (typeof value !== 'string')
32
+ return [];
33
+ return value.split(',');
34
+ })
35
+ .map(item => item.trim())
36
+ .filter(Boolean);
37
+ };
38
+ const normalizeQuality = (value) => {
39
+ const quality = normalizeFirstValue(value) || String(value || '');
40
+ if (quality === 'm4a' || quality === 'ape' || quality === 'flac') {
41
+ return quality;
42
+ }
43
+ const numericQuality = Number(quality);
44
+ if (numericQuality === 320) {
45
+ return '320';
46
+ }
47
+ if (numericQuality === 128) {
48
+ return '128';
49
+ }
50
+ return DEFAULT_QUALITY;
51
+ };
52
+ const pickPlayableDomain = (sip) => {
53
+ if (!Array.isArray(sip))
54
+ return '';
55
+ const urls = sip.filter((item) => typeof item === 'string' && item.length > 0);
56
+ return (urls.find(url => !url.startsWith('http://ws')) ||
57
+ urls.find(url => url.startsWith('https://')) ||
58
+ urls[0] ||
59
+ '');
60
+ };
61
+ const joinUrl = (domain, path) => {
62
+ if (domain.endsWith('/') && path.startsWith('/')) {
63
+ return `${domain}${path.slice(1)}`;
64
+ }
65
+ if (!domain.endsWith('/') && !path.startsWith('/')) {
66
+ return `${domain}/${path}`;
67
+ }
68
+ return `${domain}${path}`;
69
+ };
70
+ const buildPlayUrl = (domain, info, guid) => {
71
+ if (!domain)
72
+ return '';
73
+ if (info.purl) {
74
+ return joinUrl(domain, info.purl);
75
+ }
76
+ // Fallback for payloads where purl is empty but vkey/filename are present.
77
+ if (info.vkey && info.filename) {
78
+ return `${joinUrl(domain, info.filename)}?vkey=${info.vkey}&guid=${guid}&fromtag=66`;
79
+ }
80
+ return '';
81
+ };
82
+ const resolveUin = (cookie) => {
83
+ const defaultUin = String(global.userInfo?.uin || global.userInfo?.loginUin || '0');
84
+ return (0, cookieResolver_1.extractUinFromCookie)(cookie) || defaultUin;
85
+ };
86
+ const getCookieFromOptions = (option) => {
87
+ if (!option || typeof option !== 'object') {
88
+ return undefined;
89
+ }
90
+ const headers = option.headers;
91
+ if (!headers || typeof headers !== 'object') {
92
+ return undefined;
93
+ }
94
+ const cookie = headers.Cookie || headers.cookie;
95
+ return typeof cookie === 'string' ? cookie : undefined;
96
+ };
97
+ exports.default = async ({ method = 'get', params = {}, option = {} }) => {
98
+ const musicParams = params;
99
+ const songmidList = normalizeSongmidList(musicParams.songmid);
100
+ if (songmidList.length === 0) {
101
+ return {
102
+ status: 400,
103
+ body: {
104
+ data: {
105
+ message: 'no songmid'
106
+ }
107
+ }
108
+ };
109
+ }
110
+ const justPlayUrl = (normalizeFirstValue(musicParams.resType) || 'play') === 'play';
111
+ const mediaId = normalizeFirstValue(musicParams.mediaId);
112
+ const quality = normalizeQuality(musicParams.quality);
113
+ const guid = String(config_1._guid || '1429839143');
114
+ const cookie = getCookieFromOptions(option);
115
+ const uin = resolveUin(cookie);
116
+ const fileType = FILE_TYPE_MAP[quality];
117
+ const filename = songmidList.map(item => `${fileType.prefix}${item}${mediaId || item}${fileType.suffix}`);
118
+ const requestPayload = {
119
+ req_0: {
120
+ module: 'vkey.GetVkeyServer',
121
+ method: 'CgiGetVkey',
122
+ param: {
123
+ filename,
124
+ guid,
125
+ songmid: songmidList,
126
+ songtype: [0],
127
+ uin,
128
+ loginflag: 1,
129
+ platform: '20'
130
+ }
131
+ },
132
+ loginUin: uin,
133
+ comm: {
134
+ uin,
135
+ format: 'json',
136
+ ct: 24,
137
+ cv: 0
138
+ }
139
+ };
140
+ const upstreamParams = {
141
+ format: 'json',
142
+ sign: 'zzannc1o6o9b4i971602f3554385022046ab796512b7012',
143
+ data: JSON.stringify(requestPayload)
144
+ };
145
+ try {
146
+ const response = await (0, UCommon_1.default)({
147
+ method: method,
148
+ params: upstreamParams,
149
+ option
150
+ });
151
+ const upstreamData = response.data;
152
+ const domain = pickPlayableDomain(upstreamData?.req_0?.data?.sip);
153
+ const midurlinfo = Array.isArray(upstreamData?.req_0?.data?.midurlinfo)
154
+ ? upstreamData.req_0.data.midurlinfo
155
+ : [];
156
+ const playUrl = {};
157
+ const midInfoMap = new Map();
158
+ midurlinfo.forEach(item => {
159
+ if (item.songmid) {
160
+ midInfoMap.set(item.songmid, item);
161
+ }
162
+ });
163
+ songmidList.forEach(songmid => {
164
+ const item = midInfoMap.get(songmid);
165
+ const url = item ? buildPlayUrl(domain, item, guid) : '';
166
+ playUrl[songmid] = {
167
+ url,
168
+ error: url ? undefined : '\u6682\u65e0\u64ad\u653e\u94fe\u63a5'
169
+ };
170
+ });
171
+ upstreamData.playUrl = playUrl;
172
+ return {
173
+ status: 200,
174
+ body: {
175
+ data: justPlayUrl ? { playUrl } : upstreamData
176
+ }
177
+ };
178
+ }
179
+ catch (error) {
180
+ return {
181
+ status: 502,
182
+ body: {
183
+ error: error instanceof Error ? error.message : error
184
+ }
185
+ };
186
+ }
187
+ };
@@ -38,7 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  const request_1 = __importDefault(require("../../util/request"));
40
40
  const config = __importStar(require("../config"));
41
- exports.default = ({ options = {}, method = 'get' }) => {
41
+ exports.default = ({ options = {}, method = 'get', customCookie }) => {
42
42
  const opts = { ...options };
43
43
  // Merge commonParams into params for query string
44
44
  opts.params = { ...config.commonParams, ...(opts.params || {}) };
@@ -52,5 +52,11 @@ exports.default = ({ options = {}, method = 'get' }) => {
52
52
  const logOpts = { ...opts, headers: { ...opts.headers, cookie: '[REDACTED]' } };
53
53
  console.log('https://u.y.qq.com/cgi-bin/musicu.fcg', { opts: logOpts });
54
54
  }
55
- return (0, request_1.default)('https://u.y.qq.com/cgi-bin/musicu.fcg', method, opts, 'u');
55
+ return (0, request_1.default)({
56
+ url: 'https://u.y.qq.com/cgi-bin/musicu.fcg',
57
+ method: method,
58
+ options: opts,
59
+ isUUrl: 'u',
60
+ cookie: customCookie
61
+ });
56
62
  };
@@ -59,7 +59,7 @@ const getErrorMessage = (payload) => {
59
59
  // 获取用户创建的歌单
60
60
  // 注意:此接口需要有效的 QQ 音乐 Cookie 才能正常工作
61
61
  const getUserPlaylists = async (params) => {
62
- const { uin, offset = 0, limit = 30 } = params;
62
+ const { uin, offset = 0, limit = 30, cookie } = params;
63
63
  // 使用 c6.y.qq.com/rsc/fcgi-bin/fcg_get_profile_homepage.fcg 接口
64
64
  // 这是通过 Chrome DevTools 抓包发现的实际使用的接口
65
65
  const url = 'https://c6.y.qq.com/rsc/fcgi-bin/fcg_get_profile_homepage.fcg';
@@ -71,13 +71,14 @@ const getUserPlaylists = async (params) => {
71
71
  offset,
72
72
  limit,
73
73
  pageOffset,
74
- hasGlobalCookie: Boolean(global.userInfo?.cookie),
75
- cookieLength: global.userInfo?.cookie?.length || 0
74
+ hasCookie: Boolean(cookie),
75
+ cookieLength: cookie?.length || 0
76
76
  });
77
77
  const response = await (0, request_1.default)({
78
78
  url,
79
79
  method: 'GET',
80
80
  isUUrl: 'u',
81
+ cookie,
81
82
  options: {
82
83
  params: {
83
84
  _: Date.now(),
@@ -100,8 +101,7 @@ const getUserPlaylists = async (params) => {
100
101
  loginUin: Number.parseInt(uin, 10)
101
102
  },
102
103
  headers: {
103
- Referer: `https://y.qq.com/portal/profile.html?uin=${uin}`,
104
- Cookie: global.userInfo?.cookie || ''
104
+ Referer: `https://y.qq.com/portal/profile.html?uin=${uin}`
105
105
  }
106
106
  }
107
107
  });
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.getUserLikedSongs = exports.getUserAvatar = exports.getUserPlaylists = exports.checkQQLoginQr = exports.getQQLoginQr = exports.getTopLists = exports.UCommon = exports.getComments = exports.getAlbumInfo = exports.getLyric = exports.getDigitalAlbumLists = exports.getRadioLists = exports.getSingerStarNum = exports.getSingerDesc = exports.getSingerMv = exports.getSimilarSinger = exports.getMvByTag = exports.songListDetail = exports.songListCategories = exports.songLists = exports.getSmartbox = exports.getSearchByKey = exports.getHotKey = exports.downloadQQMusic = void 0;
6
+ exports.getUserLikedSongs = exports.getUserAvatar = exports.getUserPlaylists = exports.checkQQLoginQr = exports.getQQLoginQr = exports.getTopLists = exports.UCommon = exports.getComments = exports.getAlbumInfo = exports.getMusicPlay = exports.getLyric = exports.getDigitalAlbumLists = exports.getRadioLists = exports.getSingerStarNum = exports.getSingerDesc = exports.getSingerMv = exports.getSimilarSinger = exports.getMvByTag = exports.songListDetail = exports.songListCategories = exports.songLists = exports.getSmartbox = exports.getSearchByKey = exports.getHotKey = exports.downloadQQMusic = void 0;
7
7
  const downloadQQMusic_1 = __importDefault(require("./apis/downloadQQMusic"));
8
8
  exports.downloadQQMusic = downloadQQMusic_1.default;
9
9
  // search
@@ -41,6 +41,8 @@ exports.getDigitalAlbumLists = getDigitalAlbumLists_1.default;
41
41
  // music
42
42
  const getLyric_1 = __importDefault(require("./apis/music/getLyric"));
43
43
  exports.getLyric = getLyric_1.default;
44
+ const getMusicPlay_1 = __importDefault(require("./apis/music/getMusicPlay"));
45
+ exports.getMusicPlay = getMusicPlay_1.default;
44
46
  // album
45
47
  const getAlbumInfo_1 = __importDefault(require("./apis/album/getAlbumInfo"));
46
48
  exports.getAlbumInfo = getAlbumInfo_1.default;
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sansenjian/qq-music-api",
3
- "version": "2.2.7",
3
+ "version": "2.2.9",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -31,6 +31,9 @@
31
31
  "test:watch": "jest --watch",
32
32
  "test:coverage": "jest --coverage",
33
33
  "test:verbose": "jest --verbose",
34
+ "test:unit": "jest --testPathPattern=tests/unit",
35
+ "test:flags": "tsx scripts/run-tests-with-flags.ts all",
36
+ "test:flags:unit": "tsx scripts/run-tests-with-flags.ts unit",
34
37
  "docs:dev": "vitepress dev docs --port 9611",
35
38
  "docs:preview": "vitepress preview docs",
36
39
  "commit-push": "./scripts/commit-push.sh",
@@ -1,16 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const module_1 = require("../../module");
4
- const controller = async (ctx, next) => {
4
+ const util_1 = require("../util");
5
+ const apiResponse_1 = require("../../util/apiResponse");
6
+ const batchGetSongInfoController = (0, util_1.withErrorHandler)(async (ctx) => {
5
7
  const { songs } = ctx.request.body || {};
6
- const params = Object.assign({
8
+ const params = {
7
9
  format: 'json',
8
10
  inCharset: 'utf8',
9
11
  outCharset: 'utf-8',
10
12
  notice: 0,
11
13
  platform: 'yqq.json',
12
14
  needNewCode: 0
13
- });
15
+ };
14
16
  const props = {
15
17
  method: 'get',
16
18
  option: {},
@@ -18,7 +20,7 @@ const controller = async (ctx, next) => {
18
20
  };
19
21
  const data = await Promise.all((songs || []).map(async (song) => {
20
22
  const [song_mid, song_id = ''] = song;
21
- return await (0, module_1.UCommon)({
23
+ const response = await (0, module_1.UCommon)({
22
24
  ...props,
23
25
  params: {
24
26
  ...params,
@@ -38,16 +40,9 @@ const controller = async (ctx, next) => {
38
40
  }
39
41
  }
40
42
  }
41
- }).then(res => res.data);
43
+ });
44
+ return response.data;
42
45
  }));
43
- Object.assign(ctx, {
44
- status: 200,
45
- body: {
46
- response: {
47
- code: 0,
48
- data
49
- }
50
- }
51
- });
52
- };
53
- exports.default = controller;
46
+ (0, util_1.setApiResponse)(ctx, (0, apiResponse_1.customResponse)({ response: { code: 0, data } }, 200));
47
+ });
48
+ exports.default = batchGetSongInfoController;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const module_1 = require("../../module");
4
- const controller = async (ctx, next) => {
4
+ const util_1 = require("../util");
5
+ const apiResponse_1 = require("../../util/apiResponse");
6
+ const batchGetSongListsController = (0, util_1.withErrorHandler)(async (ctx) => {
5
7
  const { limit: ein = 19, page: sin = 0, sortId = 5, categoryIds = [10000000] } = ctx.request.body || {};
6
8
  const params = {
7
9
  sortId,
@@ -13,25 +15,21 @@ const controller = async (ctx, next) => {
13
15
  option: {},
14
16
  params
15
17
  };
16
- const data = await Promise.all(categoryIds.map(async (categoryId) => await (0, module_1.songLists)({
17
- ...props,
18
- params: {
19
- ...params,
20
- categoryId
21
- }
22
- }).then(res => {
23
- if (res.body.response && +res.body.response.code === 0) {
24
- return res.body.response.data;
18
+ const data = await Promise.all(categoryIds.map(async (categoryId) => {
19
+ const result = await (0, module_1.songLists)({
20
+ ...props,
21
+ params: {
22
+ ...params,
23
+ categoryId
24
+ }
25
+ });
26
+ if (result.body.response && +result.body.response.code === 0) {
27
+ return result.body.response.data;
25
28
  }
26
29
  else {
27
- return res.body.response;
28
- }
29
- })));
30
- Object.assign(ctx, {
31
- body: {
32
- status: 200,
33
- data
30
+ return result.body.response;
34
31
  }
35
- });
36
- };
37
- exports.default = controller;
32
+ }));
33
+ (0, util_1.setApiResponse)(ctx, (0, apiResponse_1.customResponse)({ status: 200, data }, 200));
34
+ });
35
+ exports.default = batchGetSongListsController;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const module_1 = require("../../module");
4
- const controller = async (ctx, next) => {
4
+ const util_1 = require("../util");
5
+ const apiResponse_1 = require("../../util/apiResponse");
6
+ const getAlbumInfoController = (0, util_1.withErrorHandler)(async (ctx) => {
5
7
  const { albummid } = ctx.query;
6
8
  const props = {
7
9
  method: 'get',
@@ -10,20 +12,11 @@ const controller = async (ctx, next) => {
10
12
  },
11
13
  option: {}
12
14
  };
13
- if (albummid) {
14
- const { status, body } = await (0, module_1.getAlbumInfo)(props);
15
- Object.assign(ctx, {
16
- status,
17
- body
18
- });
15
+ if (!albummid) {
16
+ (0, util_1.setApiResponse)(ctx, (0, apiResponse_1.errorResponse)('no albummid', 400));
17
+ return;
19
18
  }
20
- else {
21
- ctx.status = 400;
22
- ctx.body = {
23
- data: {
24
- message: 'no albummid'
25
- }
26
- };
27
- }
28
- };
29
- exports.default = controller;
19
+ const result = await (0, module_1.getAlbumInfo)(props);
20
+ (0, util_1.setApiResponse)(ctx, result);
21
+ });
22
+ exports.default = getAlbumInfoController;
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const module_1 = require("../../module");
4
- const controller = async (ctx, next) => {
4
+ const util_1 = require("../util");
5
+ const apiResponse_1 = require("../../util/apiResponse");
6
+ const getCommentsController = (0, util_1.withErrorHandler)(async (ctx) => {
5
7
  const { id, pagesize = 25, pagenum = 0, cid = 205360772, cmd = 8, reqtype = 2, biztype = 1, rootcommentid = !pagenum && '' } = ctx.query;
6
8
  const checkrootcommentid = !pagenum ? true : !!rootcommentid;
7
- const params = Object.assign({
9
+ const params = {
8
10
  cid,
9
11
  reqtype,
10
12
  biztype,
@@ -13,26 +15,17 @@ const controller = async (ctx, next) => {
13
15
  pagenum,
14
16
  pagesize,
15
17
  lasthotcommentid: rootcommentid
16
- });
18
+ };
17
19
  const props = {
18
20
  method: 'get',
19
21
  params,
20
22
  option: {}
21
23
  };
22
- if (id && checkrootcommentid) {
23
- const { status, body } = await (0, module_1.getComments)(props);
24
- Object.assign(ctx, {
25
- status,
26
- body
27
- });
28
- }
29
- else {
30
- ctx.status = 400;
31
- ctx.body = {
32
- data: {
33
- message: 'Don\'t have id or rootcommentid'
34
- }
35
- };
24
+ if (!id || !checkrootcommentid) {
25
+ (0, util_1.setApiResponse)(ctx, (0, apiResponse_1.errorResponse)('Don\'t have id or rootcommentid', 400));
26
+ return;
36
27
  }
37
- };
38
- exports.default = controller;
28
+ const result = await (0, module_1.getComments)(props);
29
+ (0, util_1.setApiResponse)(ctx, result);
30
+ });
31
+ exports.default = getCommentsController;