@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.
- package/CHANGELOG.md +14 -1
- package/README.md +2 -0
- package/dist/app.js +24 -5
- package/dist/config/service-config.js +37 -0
- package/dist/jest.config.js +14 -2
- package/dist/middlewares/fallback-middleware.js +29 -0
- package/dist/module/apis/music/getMusicPlay.js +187 -0
- package/dist/module/apis/u_common.js +8 -2
- package/dist/module/apis/user/getUserPlaylists.js +5 -5
- package/dist/module/index.js +3 -1
- package/dist/package.json +4 -1
- package/dist/routers/context/batchGetSongInfo.js +11 -16
- package/dist/routers/context/batchGetSongLists.js +18 -20
- package/dist/routers/context/getAlbumInfo.js +10 -17
- package/dist/routers/context/getComments.js +12 -19
- package/dist/routers/context/getDailyRecommend.js +5 -17
- package/dist/routers/context/getHotkey.js +7 -8
- package/dist/routers/context/getMusicPlay.js +23 -81
- package/dist/routers/context/getMv.js +16 -22
- package/dist/routers/context/getMvPlay.js +48 -49
- package/dist/routers/context/getPersonalRecommend.js +13 -25
- package/dist/routers/context/getPlaylistTags.js +20 -26
- package/dist/routers/context/getRanks.js +9 -16
- package/dist/routers/context/getSingerAlbum.js +16 -22
- package/dist/routers/context/getSingerHotsong.js +16 -22
- package/dist/routers/context/getSingerList.js +9 -21
- package/dist/routers/context/getSongInfo.js +9 -16
- package/dist/routers/context/getSongListCategories.js +6 -7
- package/dist/routers/context/getSongListDetail.js +6 -8
- package/dist/routers/context/getUserAvatar.js +20 -33
- package/dist/routers/context/getUserPlaylists.js +6 -3
- package/dist/routers/context/index.js +94 -103
- package/dist/routers/util.js +31 -0
- package/dist/scripts/run-tests-with-flags.js +139 -0
- package/dist/util/cookie.js +15 -7
- package/dist/util/cookieResolver.js +66 -0
- package/dist/util/request.js +15 -6
- package/docs-dist/404.html +2 -2
- package/docs-dist/CHANGELOG-ARCHITECTURE.html +6 -6
- package/docs-dist/COOKIE_CONFIG_GUIDE.html +6 -6
- package/docs-dist/FALLBACK_MODE_GUIDE.html +101 -0
- package/docs-dist/README.html +6 -6
- package/docs-dist/TEST_USER_PLAYLISTS.html +6 -6
- package/docs-dist/USER_AVATAR_GUIDE.html +6 -6
- package/docs-dist/api/comments.html +6 -6
- package/docs-dist/api/index.html +6 -6
- package/docs-dist/api/music.html +6 -6
- package/docs-dist/api/other.html +6 -6
- package/docs-dist/api/playlist.html +6 -6
- package/docs-dist/api/rank.html +6 -6
- package/docs-dist/api/search.html +6 -6
- package/docs-dist/api/singer.html +6 -6
- package/docs-dist/api/user.html +6 -6
- package/docs-dist/assets/{CHANGELOG-ARCHITECTURE.md.BOe0ZtyR.js → CHANGELOG-ARCHITECTURE.md.r40JGJZK.js} +1 -1
- package/docs-dist/assets/{CHANGELOG-ARCHITECTURE.md.BOe0ZtyR.lean.js → CHANGELOG-ARCHITECTURE.md.r40JGJZK.lean.js} +1 -1
- package/docs-dist/assets/{COOKIE_CONFIG_GUIDE.md.D68AwXR2.js → COOKIE_CONFIG_GUIDE.md.BVXl7WHu.js} +1 -1
- package/docs-dist/assets/{COOKIE_CONFIG_GUIDE.md.D68AwXR2.lean.js → COOKIE_CONFIG_GUIDE.md.BVXl7WHu.lean.js} +1 -1
- package/docs-dist/assets/FALLBACK_MODE_GUIDE.md.BBdcIdh_.js +75 -0
- package/docs-dist/assets/FALLBACK_MODE_GUIDE.md.BBdcIdh_.lean.js +1 -0
- package/docs-dist/assets/{README.md.ZJQGJ1Gb.js → README.md.D6Tw0nRd.js} +1 -1
- package/docs-dist/assets/{README.md.ZJQGJ1Gb.lean.js → README.md.D6Tw0nRd.lean.js} +1 -1
- package/docs-dist/assets/{TEST_USER_PLAYLISTS.md.C02575X2.js → TEST_USER_PLAYLISTS.md.DSt20Igj.js} +1 -1
- package/docs-dist/assets/{TEST_USER_PLAYLISTS.md.C02575X2.lean.js → TEST_USER_PLAYLISTS.md.DSt20Igj.lean.js} +1 -1
- package/docs-dist/assets/{USER_AVATAR_GUIDE.md.BOqjn5Cm.js → USER_AVATAR_GUIDE.md.CVHPs2Dn.js} +1 -1
- package/docs-dist/assets/{USER_AVATAR_GUIDE.md.BOqjn5Cm.lean.js → USER_AVATAR_GUIDE.md.CVHPs2Dn.lean.js} +1 -1
- package/docs-dist/assets/{api_comments.md.DADvndEA.js → api_comments.md.79Q_C8Qp.js} +1 -1
- package/docs-dist/assets/{api_comments.md.DADvndEA.lean.js → api_comments.md.79Q_C8Qp.lean.js} +1 -1
- package/docs-dist/assets/{api_index.md.D5IASxxG.js → api_index.md.CU3By8tw.js} +1 -1
- package/docs-dist/assets/{api_index.md.D5IASxxG.lean.js → api_index.md.CU3By8tw.lean.js} +1 -1
- package/docs-dist/assets/{api_music.md.BgB8NmZq.js → api_music.md.B1AzLePX.js} +1 -1
- package/docs-dist/assets/{api_music.md.BgB8NmZq.lean.js → api_music.md.B1AzLePX.lean.js} +1 -1
- package/docs-dist/assets/{api_other.md.BkRWXX2z.js → api_other.md.DCg4bzA7.js} +1 -1
- package/docs-dist/assets/{api_other.md.BkRWXX2z.lean.js → api_other.md.DCg4bzA7.lean.js} +1 -1
- package/docs-dist/assets/{api_playlist.md.Dc0hTrZ4.js → api_playlist.md.8ACJ3QqD.js} +1 -1
- package/docs-dist/assets/{api_playlist.md.Dc0hTrZ4.lean.js → api_playlist.md.8ACJ3QqD.lean.js} +1 -1
- package/docs-dist/assets/{api_rank.md.DRisCFyT.js → api_rank.md.B8IP2ZRy.js} +1 -1
- package/docs-dist/assets/{api_rank.md.DRisCFyT.lean.js → api_rank.md.B8IP2ZRy.lean.js} +1 -1
- package/docs-dist/assets/{api_search.md.DNnMUZK0.js → api_search.md.DO9J6nvp.js} +1 -1
- package/docs-dist/assets/{api_search.md.DNnMUZK0.lean.js → api_search.md.DO9J6nvp.lean.js} +1 -1
- package/docs-dist/assets/{api_singer.md.DCmuxQkk.js → api_singer.md.CcL32xuN.js} +1 -1
- package/docs-dist/assets/{api_singer.md.DCmuxQkk.lean.js → api_singer.md.CcL32xuN.lean.js} +1 -1
- package/docs-dist/assets/{api_user.md.Cjm9GG3z.js → api_user.md.Cb7Ky3Sn.js} +1 -1
- package/docs-dist/assets/{api_user.md.Cjm9GG3z.lean.js → api_user.md.Cb7Ky3Sn.lean.js} +1 -1
- package/docs-dist/assets/{app.Dx_1wB58.js → app.CSainqD9.js} +1 -1
- package/docs-dist/assets/chunks/@localSearchIndexroot.BKleDIv-.js +1 -0
- package/docs-dist/assets/chunks/{VPLocalSearchBox.DwKWtsdX.js → VPLocalSearchBox.BUBaq7tw.js} +1 -1
- package/docs-dist/assets/chunks/framework.aJbMEiY9.js +19 -0
- package/docs-dist/assets/chunks/{theme.pGVgJ9Cx.js → theme.CzMhU0Ps.js} +2 -2
- package/docs-dist/assets/{guide_architecture.md.DGtNyuMH.js → guide_architecture.md.CzgqynmB.js} +1 -1
- package/docs-dist/assets/{guide_architecture.md.DGtNyuMH.lean.js → guide_architecture.md.CzgqynmB.lean.js} +1 -1
- package/docs-dist/assets/{guide_authentication.md.mtI5LfCw.js → guide_authentication.md.a8yTA8Xe.js} +1 -1
- package/docs-dist/assets/{guide_authentication.md.mtI5LfCw.lean.js → guide_authentication.md.a8yTA8Xe.lean.js} +1 -1
- package/docs-dist/assets/{guide_index.md.B-0SG46T.js → guide_index.md.BgUUL6fI.js} +1 -1
- package/docs-dist/assets/{guide_index.md.B-0SG46T.lean.js → guide_index.md.BgUUL6fI.lean.js} +1 -1
- package/docs-dist/assets/{guide_installation.md.k-KpAfxv.js → guide_installation.md.BCZ4jBl_.js} +1 -1
- package/docs-dist/assets/guide_installation.md.BCZ4jBl_.lean.js +1 -0
- package/docs-dist/assets/{guide_quickstart.md.Bff_KFOD.js → guide_quickstart.md.9-4dA6wS.js} +1 -1
- package/docs-dist/assets/guide_quickstart.md.9-4dA6wS.lean.js +1 -0
- package/docs-dist/assets/{index.md.xrs-uIyo.js → index.md.z0hAJioN.js} +1 -1
- package/docs-dist/assets/{index.md.xrs-uIyo.lean.js → index.md.z0hAJioN.lean.js} +1 -1
- package/docs-dist/assets/{reference_response-format.md.DKYTK6uJ.js → reference_response-format.md.VvQTLDZr.js} +1 -1
- package/docs-dist/assets/{reference_response-format.md.DKYTK6uJ.lean.js → reference_response-format.md.VvQTLDZr.lean.js} +1 -1
- package/docs-dist/guide/architecture.html +6 -6
- package/docs-dist/guide/authentication.html +6 -6
- package/docs-dist/guide/index.html +6 -6
- package/docs-dist/guide/installation.html +6 -6
- package/docs-dist/guide/quickstart.html +6 -6
- package/docs-dist/hashmap.json +1 -1
- package/docs-dist/index.html +5 -5
- package/docs-dist/reference/response-format.html +6 -6
- package/docs-dist/version.json +3 -3
- package/package.json +4 -1
- package/docs-dist/assets/chunks/@localSearchIndexroot.CMY5EIwU.js +0 -1
- package/docs-dist/assets/chunks/framework.o40iizuP.js +0 -19
- package/docs-dist/assets/guide_installation.md.k-KpAfxv.lean.js +0 -1
- package/docs-dist/assets/guide_quickstart.md.Bff_KFOD.lean.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
## 2.2.
|
|
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
|

|
|
10
10
|
<br />
|
|
11
11
|
      
|
|
12
|
+
<br />
|
|
13
|
+
[](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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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 (!
|
|
26
|
-
console.log(chalk_1.default.yellow(
|
|
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;
|
package/dist/jest.config.js
CHANGED
|
@@ -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)(
|
|
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
|
-
|
|
75
|
-
cookieLength:
|
|
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
|
});
|
package/dist/module/index.js
CHANGED
|
@@ -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.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
})
|
|
43
|
+
});
|
|
44
|
+
return response.data;
|
|
42
45
|
}));
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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) =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
28
|
-
}
|
|
29
|
-
})));
|
|
30
|
-
Object.assign(ctx, {
|
|
31
|
-
body: {
|
|
32
|
-
status: 200,
|
|
33
|
-
data
|
|
30
|
+
return result.body.response;
|
|
34
31
|
}
|
|
35
|
-
});
|
|
36
|
-
};
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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 =
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
+
const result = await (0, module_1.getComments)(props);
|
|
29
|
+
(0, util_1.setApiResponse)(ctx, result);
|
|
30
|
+
});
|
|
31
|
+
exports.default = getCommentsController;
|