Compare commits

...
150 Commits
Author SHA1 Message Date
Takanashi Hoshino 19c7dad6db docs: 修正文档 2026-10-17 19:29:09 +08:00
Takanashi Hoshino 2db6684453 feat: 添加日推风格相关接口 (90de64140f) 2026-10-05 22:41:37 +08:00
Takanashi Hoshino 90de64140f feat: 添加日推风格相关接口 2026-10-05 22:38:41 +08:00
Takanashi Hoshino 2aab9957df bump 4.41.0 2026-10-03 20:50:35 +08:00
7N21 373d5eedb3 Merge pull request #278 from MT-SUPER-POWER/main
乐迷团相关接口补充
2026-10-03 20:45:47 +08:00
MT-SUPER-POWER c1cbdcb962 Merge remote-tracking branch 'upstream/HEAD' 2026-10-03 18:46:54 +08:00
MT-SUPER-POWER 37b64ab01e feat: 添加了'乐迷团'的相关接口 2026-10-03 18:42:36 +08:00
7N21 95367213f7 Merge pull request #276 from 1254qwer/fix/xeapi/csr
fix: xeapi 请求从 BSR 改为 CSR
2026-10-03 17:35:18 +08:00
1254qwer bd407cee81 fix: xeapi from BSR to CSR 2026-10-03 17:21:32 +08:00
7N21 a91ea8f832 Merge pull request #265 from cmachsocket/pr
compatibility fix of rumtime and legacy node
2026-10-02 23:44:50 +08:00
Takanashi Hoshino 4a45a14aa7 build: update denpendences 8ad30b591d 2026-10-01 18:19:20 +08:00
7N21 acbea014be Merge branch 'main' into pr 2026-10-01 16:02:25 +08:00
Takanashi Hoshino 8ad30b591d build: update dependences 2026-10-01 13:49:53 +08:00
Takanashi Hoshino cb449463fb Merge remote-tracking branch 'origin/dependabot/github_actions/actions/stale-11' 2026-09-30 22:33:18 +08:00
Takanashi Hoshino 91db12d0f5 Merge remote-tracking branch 'origin/dependabot/github_actions/actions/create-github-app-token-3' 2026-09-30 22:33:06 +08:00
7N21 135df9edda Merge pull request #274 from 1254qwer/feat/neapi
feat: 新增 neapi 算法支持
2026-09-30 20:19:55 +08:00
dependabot[bot] 548f6c170a build(deps): bump actions/create-github-app-token from 1 to 3
Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1 to 3.
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Changelog](https://github.com/actions/create-github-app-token/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/create-github-app-token/compare/v1...v3)

---
updated-dependencies:
- dependency-name: actions/create-github-app-token
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-09-30 11:57:54 +00:00
Takanashi Hoshino 17c07960f9 remove ci 2026-09-30 19:56:15 +08:00
1254qwer c4d448641a feat: Add neapi support 2026-09-27 14:53:29 +08:00
cmachsocket 77a9269828 fix lint 2026-09-19 21:31:39 +08:00
cmachsocket 9a16c89714 compatibility fix of rumtime and legacy node 2026-09-14 21:13:16 +08:00
7N21 a8c781fd64 Merge pull request #260 from Cosmosuperbaka/main
feat: 新增登录设备管理中设备名称上报接口
2026-09-12 00:46:53 +08:00
Cosmosuperbaka 690b462577 docs: 补充设备名称上报接口的文档与类型声明 2026-09-12 00:38:43 +08:00
Cosmosuperbaka 25ac0c041f fix: 设备名称上报接口补充必填参数校验 2026-09-11 20:36:43 +08:00
Cosmosuperbaka edef19c3fb feat: 新增设备中心设备名称上报接口 2026-09-10 16:07:54 +08:00
Takanashi Hoshino d55d92cd00 ci: switch model 2026-09-04 20:01:15 +08:00
dependabot[bot] d9253bf930 build(deps): bump actions/stale from 10 to 11
Bumps [actions/stale](https://github.com/actions/stale) from 10 to 11.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v10...v11)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: '11'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-09-01 12:47:33 +00:00
MoeFurina f5ce55bcb4 build: update dependence 2026-08-27 23:51:49 +08:00
MoeFurina 4480bd4359 ci: switch pnpm 9 2026-08-27 23:43:19 +08:00
MoeFurina e43b123c8a ci: fix pnpm 11 not work 2026-08-27 23:31:52 +08:00
MoeFurina ed9bdd6ffa ci: fix pnpm 11 not work 2026-08-27 23:14:52 +08:00
MoeFurina 7b733e54ab ci: fix pnpm 11 not work 2026-08-27 22:59:30 +08:00
MoeFurina edfaf6c0d1 ci: switch another model 2026-08-27 22:47:35 +08:00
Takanashi Hoshino f83ad951f6 Merge pull request #243 from cXp1r/main
fix: 修正NMTID来源
2026-08-25 12:22:34 +08:00
cXp1r 22003c2c32 fix: 修复NMTID保底有随机生成导致服务器永远不会下放, 致使重复进入分支浪费性能 2026-08-25 00:51:47 +08:00
cXp1r 8376699f5e fix: 修改NMTID采取逻辑和匹配逻辑 2026-08-24 17:46:19 +08:00
cXp1r 5c98a9a1ea fix: 修正NMTID来源 2026-08-24 16:26:12 +08:00
MoeFurina 7c17de7f09 ci: switch another model 2026-08-22 12:47:42 +08:00
Takanashi Hoshino a7e8d485f3 Merge pull request #242 from SteveCodeFox/main
fix: 音质相关说明
2026-08-20 18:10:41 +08:00
SteveCodeFox 0957be6e76 fix: "高清全景声" => "高清臻音"
add: "ste2", "c512", "aac2"三种沉浸声类型说明
2026-08-20 13:19:50 +08:00
MoeFurina 4a91f77e62 ci: fix pnpm has no pnpm-workspace.yaml 2026-08-18 20:18:41 +08:00
MoeFurina a7437f7ca4 feat: 更新歌曲相关接口 2026-08-18 20:11:30 +08:00
Takanashi Hoshino 498945c401 Merge pull request #241 from 1254qwer/fix/song-url-v1
fix: 支持获取臻音全景声
2026-08-18 19:30:02 +08:00
MoeFurina 7b17a08628 docs: 补全私人漫游模式文档内容 2026-08-18 19:23:30 +08:00
1254qwer 0b5c3927c9 fix: /song/url/v1/302 & /song/download/url/v1 支持臻音全景声 2026-08-17 09:15:51 +08:00
1254qwer 881267c56d fix: /song/url/v1 支持臻音全景声 2026-08-16 19:08:38 +08:00
MoeFurina d7c0db9455 fix: 错误的docker配置 2026-08-09 11:36:53 +08:00
Takanashi Hoshino c1d14a86d9 Merge pull request #237 from NeteaseCloudMusicApiEnhanced/fix/yidun
refactor: 重写checkToken V2模块
2026-08-09 11:21:39 +08:00
MoeFurina a2a2a40d71 fixes 2026-08-07 13:45:01 +08:00
MoeFurina 543660fcdf fix: pkg打包node18兼容 2026-08-07 12:13:43 +08:00
MoeFurina 98a95002b2 fix: node18兼容 2026-08-07 12:01:11 +08:00
MoeFurina c4a3d83abe refactor: 重写checkToken V2模块 2026-08-07 11:52:54 +08:00
Takanashi Hoshino 7753b5de41 Merge pull request #235 from NeteaseCloudMusicApiEnhanced/feat/yunbei_task
feat: 新增云贝任务相关接口
2026-08-06 16:58:07 +08:00
MoeFurina c1fe1eb925 Merge branch 'pr/235' 2026-08-06 16:55:27 +08:00
MoeFurina 3357317a1d upd 2026-08-06 16:51:51 +08:00
MoeFurina a08f425ae7 Merge branch 'main' into feat/yunbei_task 2026-08-06 16:45:11 +08:00
MoeFurina 5b780addba ci: test ci with the workflow file 2026-08-05 20:44:21 +08:00
MoeFurina fc9f41839c ci: test ci with workflow file 2026-08-05 19:44:08 +08:00
MoeFurina 5859944563 format docs 2026-08-05 18:31:49 +08:00
MoeFurina aaa6459e55 remove disclaimer 2026-08-05 18:13:14 +08:00
MoeFurina a898e1d86c feat: 新增云贝任务相关接口 2026-08-05 18:11:31 +08:00
MoeFurina 4045f1ad3f Merge branch 'pr/227' 2026-08-02 16:40:25 +08:00
MoeFurina e6214a3f73 Merge branch 'pr/229' 2026-08-02 16:39:14 +08:00
MoeFurina 686e8bf969 revert: revert some changes 2026-08-02 16:37:46 +08:00
anupamme 9214cb945f fix: V-001 security vulnerability
Automated security fix generated by OrbisAI Security
2026-08-02 08:35:02 +00:00
MoeFurina a79a2d9ef6 fix: 修正workflow文件 2026-08-02 16:19:40 +08:00
7788dev 9608da9b96 fix(voice): load created podcasts in upload example 2026-08-02 12:52:25 +08:00
7788dev 93b3e624d8 feat(voice): support cover image uploads 2026-08-02 11:53:51 +08:00
MoeFurina 15f515c1ba ci(test): test ci with the workflow file 2026-08-02 11:27:45 +08:00
anupamme e255e8f334 fix: V-001 security vulnerability
Automated security fix generated by OrbisAI Security
2026-08-02 03:16:22 +00:00
MoeFurina 16aa61f98e ci(test): test ci workflow file 2026-08-02 11:00:13 +08:00
MoeFurina a939003b92 ci(test): test ci workflow file 2026-08-02 10:46:19 +08:00
MoeFurina 8f4873f2e2 perf: 更新接口定义 2026-08-01 19:11:37 +08:00
MoeFurina a8b45a8b8b Merge branch 'pr/225' 2026-08-01 17:34:54 +08:00
MoeFurina 1a70281c76 ci: 更新问题管理工作流 2026-08-01 17:20:19 +08:00
tanjuntao f5f4ff6eda feat: add complete current-user event listing 2026-08-01 10:25:00 +08:00
tanjuntao eb1b5ba0ea feat: add event privacy endpoint 2026-08-01 10:25:00 +08:00
MoeFurina 6732fc7c32 ci(test): test ci workflow file 2026-07-31 22:21:34 +08:00
MoeFurina 6ce6b84015 fix: 修复分享笔记接口 2026-07-26 22:39:58 +08:00
MoeFurina 63d89aa906 bump 4.39.0 2026-07-24 17:20:38 +08:00
MoeFurina 8c212be86b docs: 删减合并冲突出现的问题 2026-07-24 17:17:23 +08:00
MoeFurina 89de56d2a2 Merge pull request #215 from ZxwyWebSite/feat-lottery 2026-07-24 17:13:54 +08:00
MoeFurina d298025d7c Merge branch 'pr/215' 2026-07-24 17:12:50 +08:00
MoeFurina a3287a0180 Merge branch 'feat-lottery' of https://github.com/ZxwyWebSite/api-enhanced into pr/215 2026-07-24 17:11:34 +08:00
MoeFurina 9adfd50c58 Merge pull request #215 from ZxwyWebSite/feat-lottery 2026-07-24 17:08:42 +08:00
MoeFurina fda7c9e5b3 Merge branch 'pr/215' 2026-07-24 16:55:29 +08:00
MoeFurina a8266c6d7b feat: 显示器显示剩余时长 2026-07-24 16:48:53 +08:00
Zxwy 64a6b9677a feat: 新增云小编抽奖接口 2026-07-23 11:14:35 +08:00
MoeFurina 20d3a1a7a5 feat: 新增获取免费听市场状态 2026-07-22 20:07:08 +08:00
MoeFurina 4826c65042 feat: weapi支持checkToken 2026-07-22 19:40:12 +08:00
MoeFurina 7822ab08d3 feat: 新增评论相关接口 2026-07-22 19:18:18 +08:00
Takanashi Hoshino 41bd6d82ce Merge pull request #216 from Suxiaoqinx/main
feat(song/url-v1): 新增immerseType参数支持并更新文档
2026-07-19 21:20:40 +08:00
苏晓晴 6f8dd1d971 删除 package-lock.json 2026-07-19 17:45:39 +08:00
Suxiaoqinx ddd0af5474 修复(song_url_v1): 删除非sky等级的immerseType处理逻辑
移除原代码中非sky等级下处理query.immerseType的代码分支,仅在level为sky时设置immerseType参数
2026-07-19 10:04:08 +08:00
Suxiaoqinx fbd2e06419 feat(song/url-v1): 新增immerseType参数支持并更新文档
补充sky音质下immerseType参数的处理逻辑,更新文档说明与调用示例,注明参数仅在level为sky时生效
2026-07-19 09:54:55 +08:00
Zxwy 4442fb4341 feat: 新增云小编抽奖接口 2026-07-18 15:25:15 +08:00
MoeFurina 35d1c61cb4 revert: some changes 2026-07-17 20:41:49 +08:00
MoeFurina a183e153ef Merge branch 'pr/214' 2026-07-17 20:11:37 +08:00
MoeFurina 2fa78c3826 fix: 修复评论接口 2026-07-17 20:11:20 +08:00
Zxwy 5ae1eab14c feat: 新增云小编接口 2026-07-17 19:02:16 +08:00
MoeFurina 321c25bd7d style: 简化createOption 2026-07-15 21:06:50 +08:00
MoeFurina f527c1ef73 docs: 新增免费听接口文档 2026-07-15 16:23:25 +08:00
MoeFurina 0a5a1242f2 feat: 新增免费听接口 2026-07-15 16:14:19 +08:00
MoeFurina 6946dc8e14 docs: 修改运行要求 2026-07-12 10:42:50 +08:00
MoeFurina 22b78779ca Merge branch 'main' of https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced 2026-07-11 17:20:25 +08:00
MoeFurina 35607f5050 perf: 改进api调试页面 2026-07-11 17:20:14 +08:00
Takanashi Hoshino 60af95ac06 Merge pull request #210 from laoshuikaixue/feature/artist-new-song-playall
feat(artist): 添加关注歌手新歌播放全部&新作品接口
2026-07-11 17:04:55 +08:00
LaoShui 46073e5f50 docs(docs): 更新 sourceType 参数说明
- 明确 `1` 表示新歌,其他取值待确认
- 添加默认值 `1` 的标注
2026-07-10 23:07:31 +08:00
LaoShui bf12abe2f1 feat(artist): 添加关注歌手新作品(歌曲/MV)接口
- 新增 module/artist_new_song_mv_list_v2.js 模块实现接口逻辑
- 在文档中添加接口说明、参数详情和调用示例
- 在 TypeScript 定义文件中添加对应的接口类型声明
- 支持分页获取关注歌手的新歌曲和 MV 列表
- 实现时间游标和翻页功能支持
- 提供多种可选参数配置选项
2026-07-10 19:20:54 +08:00
LaoShui 1491b91829 feat(artist): 添加关注歌手新歌播放全部功能
- 新增 module/artist_new_song_playall.js 文件实现获取所有关注歌手最近50首新歌的功能
- 在文档中添加关注歌手最近新歌播放全部接口说明和调用示例
- 在类型定义文件中添加 artist_new_song_playall 函数声明
2026-07-10 18:56:50 +08:00
MoeFurina 63f669d7f8 fix: 黑胶乐签签到失败 (#197) 2026-06-30 14:34:58 +08:00
Furina's Little Ocean Act Theater d525204dda Merge pull request #199 from zhuixingzhe-baisheng/add-vip-growthpoint-getall
feat: add vip_growthpoint_getall - one-click claim all VIP growth rew…
2026-06-29 16:14:42 +08:00
Furina's Little Ocean Act Theater 6eb31984fd Merge pull request #200 from floatwoozy/feat/listen-data-song-play-rank
feat: 新增 listen_data_song_play_rank 歌曲播放排行接口
2026-06-29 16:14:11 +08:00
MoeFurina 9faeb0aa25 docs: 删减文档内容 2026-06-29 15:53:01 +08:00
floatwoozy f6db3e76e9 feat: 新增 listen_data_song_play_rank 歌曲播放排行接口
新增 /api/content/activity/listen/data/song/play/rank,返回某周/月
听歌排行(每首带 playCount),默认 Top20、不足则返回全部。与已有的
listen_data_today_song(今日)同族,补全「带次数的历史周/月榜」。

- module/listen_data_song_play_rank.js:type(week|month) + endTime 参数
- interface.d.ts:补类型声明
- public/docs/home.md:补接口文档
2026-06-25 03:25:20 +08:00
MonkeyCode Agent 9163d48f0a feat: add vip_growthpoint_getall - one-click claim all VIP growth rewards (xeapi)
- New module: module/vip_growthpoint_getall.js
- Uses xeapi encryption for direct  endpoint
- No parameters required, claims all completed task rewards at once
- Added TypeScript type declaration in interface.d.ts
- Added API documentation in public/docs/home.md
2026-06-21 05:24:35 +00:00
MoeFurina 1454ee216c fix: 请求异常 (83ee2cda34) 2026-06-20 23:28:08 +08:00
MoeFurina 83ee2cda34 feat: add NCBL encrypted scrobble interface and update related documentation
- Implemented new scrobble endpoint for NCBL encrypted logs in `scrobble_v1.js`.
- Updated `scrobble.js` to use domain from config.
- Enhanced `scrobble.html` with tabbed interface for original and NCBL versions, including new input fields.
- Added cookie handling and quick fill functionality for both versions.
- Updated `config.json` to include new domain configurations.
- Introduced `ncbl.js` for NCBL encryption utilities.
- Improved documentation in `home.md` to reflect changes in API usage.
2026-06-20 22:51:51 +08:00
MoeFurina 3c352b5752 feat: 新增听歌打卡示例页面 2026-06-20 21:16:14 +08:00
MoeFurina 60ee927e32 fix: 修复听歌打卡接口 (#167) 2026-06-19 15:11:31 +08:00
MoeFurina eb07a525eb feat: 支持显示加密方式 2026-06-18 23:46:11 +08:00
MoeFurina 6fc4231142 feat(docs-format): 增加文档格式化工具 2026-06-18 23:05:44 +08:00
MoeFurina fa69736300 revert: some changes
This reverts commit c959866436.
2026-06-18 22:52:35 +08:00
MoeFurina 73a5e9d788 Revert "feat: 增加文档格式化工具"
This reverts commit 6550c7c827.
2026-06-18 22:51:34 +08:00
MoeFurina dc6f56e8a8 Revert "fix(deploy): 修复部分启动失败 (#196)"
This reverts commit 395a80e74b.
2026-06-18 22:50:53 +08:00
MoeFurina 2c9da8507d Revert "refactor: 优化serverless入口文件"
This reverts commit 1db354464f.
2026-06-18 22:50:51 +08:00
MoeFurina 1db354464f refactor: 优化serverless入口文件 2026-06-18 22:29:24 +08:00
MoeFurina 395a80e74b fix(deploy): 修复部分启动失败 (#196) 2026-06-18 21:00:06 +08:00
MoeFurina 6550c7c827 feat: 增加文档格式化工具 2026-06-13 23:49:30 +08:00
MoeFurina c959866436 feat: 增强配置生成逻辑,添加重试机制和日志记录 2026-06-13 21:21:33 +08:00
MoeFurina e3a1c041b6 fix: 修复游客登陆接口 2026-06-09 00:09:48 +08:00
MoeFurina e2324e26da Merge remote-tracking branch 'origin/dependabot/github_actions/docker/login-action-4' 2026-06-09 00:09:09 +08:00
MoeFurina f01e770c35 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/types/node-25.9.1' 2026-06-09 00:08:41 +08:00
MoeFurina abb56ec014 Merge remote-tracking branch 'origin/dependabot/github_actions/aormsby/Fork-Sync-With-Upstream-action-3.4.3' 2026-06-09 00:08:23 +08:00
dependabot[bot] 0300c1b51e build(packages): bump @types/node from 25.5.0 to 25.9.2
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.5.0 to 25.9.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-06-06 07:42:44 +00:00
MoeFurina 9896242333 bump 4.35.0 2026-06-06 15:40:08 +08:00
MoEFusion e3812be97b Merge pull request #182 from zhuixingzhe-baisheng/feat/add-relay-play-state-submit
feat: add relay_play_state_submit API for song play state tracking
2026-06-06 15:36:27 +08:00
MoeFurina e7ff0da4ba refactor: 自动生成sessionID 2026-06-06 15:33:15 +08:00
MoeFurina 65261fa658 Merge branch 'main' into pr/182 2026-06-06 15:23:37 +08:00
MoEFusionandgemini-code-assist[bot] cb5b2e7d0f Apply suggestions from code review
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-06-06 15:22:25 +08:00
MoeFurina f58fd4b50d feat: 新增云盘歌曲下载链接接口 (1a5f91a192) 2026-06-06 15:20:06 +08:00
MoeFurina 1a5f91a192 feat: 新增从云盘获取歌曲下载链接接口 2026-06-06 15:17:28 +08:00
MoeFurina 50cc26f297 feat: 为xeapi重构相关接口以优化密钥获取流程 2026-06-06 14:52:33 +08:00
MoeFurina c92613b19e fix: 更新GitHub Actions和Dockerfile中的依赖版本 2026-05-30 20:58:03 +08:00
MoeFurina 7aacc8990b feat: 新增和改进黑胶乐签相关接口 2026-05-30 20:45:24 +08:00
MoeFurina 8e934789a1 ci: 修复Dockerfile的错误配置 2026-05-30 20:17:22 +08:00
MoeFurina d26e6b0615 feat: 新增新版会员任务接口 (#173) 2026-05-30 20:02:00 +08:00
dependabot[bot] a9c33de182 chore(deps): bump docker/login-action from 3 to 4
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-05-30 10:24:02 +00:00
Developer 39e0e0718d feat: add relay_play_state_submit API for song play state tracking
- Add new interface /api/relay/play/state/submit
- Support sessionId tracking for play sessions
- Support playMode parameter (list_loop, single_loop, random, single)
- Add parameter validation
- Include integration documentation
2026-05-28 13:00:21 +00:00
dependabot[bot] 6175d55f89 chore(deps): bump aormsby/Fork-Sync-With-Upstream-action
Bumps [aormsby/Fork-Sync-With-Upstream-action](https://github.com/aormsby/fork-sync-with-upstream-action) from 3.4.2 to 3.4.3.
- [Release notes](https://github.com/aormsby/fork-sync-with-upstream-action/releases)
- [Commits](https://github.com/aormsby/fork-sync-with-upstream-action/compare/v3.4.2...v3.4.3)

---
updated-dependencies:
- dependency-name: aormsby/Fork-Sync-With-Upstream-action
  dependency-version: 3.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-05-01 14:54:43 +00:00
131 changed files with 10838 additions and 2218 deletions
+5
View File
@@ -0,0 +1,5 @@
# CodeGraph data files — local to each machine, not for committing.
# Ignore everything in .codegraph/ except this file itself, so transient
# files (the database, daemon.pid, sockets, logs) never show up in git.
*
!.gitignore
+9 -8
View File
@@ -14,15 +14,16 @@ body:
id: terms id: terms
attributes: attributes:
label: 确认事项 label: 确认事项
description: 在提交功能请求前,请确认以下事项 description: |
在提交功能请求前,请确认以下事项
- 我已经搜索了现有的issues,确认这不是重复请求
- 我已经查看了项目文档
- 该项目符合该项目的目标和范围
- 我确认我提供的接口请求是合理且有意义的
options: options:
- label: 我已经搜索了现有的issues,确认这不是重复请求 - label: 我已确认以上事项
required: true
- label: 我已经查看了项目文档
required: true
- label: 该项目符合该项目的目标和范围
required: true
- label: 我确认我提供的接口请求是合理且有意义的
required: true required: true
+4 -3
View File
@@ -34,10 +34,11 @@ jobs:
with: with:
node-version: '18' node-version: '18'
- name: Setup pnpm
run: npm install -g pnpm@9
- name: Install dependencies - name: Install dependencies
run: | run: pnpm install --frozen-lockfile
npm install -g pnpm
pnpm install
- name: Build for ${{ matrix.platform }} - name: Build for ${{ matrix.platform }}
run: | run: |
+67
View File
@@ -0,0 +1,67 @@
name: CI
on:
push:
branches: [main]
pull_request:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
check:
name: Lint / Unit / Docs
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 22, 24]
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
with:
version: 9
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: pnpm
# frozen-lockfile: 顺带校验 package.json 与 pnpm-lock.yaml 是否一致
- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
# 纯单测,不需要网络,稳定必过
- name: Unit tests
run: pnpm exec mocha -r intelli-espower-loader -t 60000 main.test.js --exit
- name: Docs format check
run: pnpm docs:check
integration:
# server.test.js 会启动真实服务器并请求真实网易云 API,
# 可能因上游风控/限流偶发失败,因此不阻塞合并
name: Integration tests (real NetEase API)
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
with:
version: 9
- uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: Run full test suite
run: pnpm test
+4 -29
View File
@@ -6,8 +6,6 @@ permissions:
on: on:
issues: issues:
types: [opened, labeled] types: [opened, labeled]
issue_comment:
types: [created]
jobs: jobs:
welcome-new-issues: welcome-new-issues:
@@ -19,44 +17,21 @@ jobs:
with: with:
issue-number: ${{ github.event.issue.number }} issue-number: ${{ github.event.issue.number }}
body: | body: |
## 快速链接 感谢您在该仓库提出问题!我们非常重视您的反馈,并希望尽快为您提供帮助。
在我们处理您的问题之前,您可以先查看以下资源: 如果您在一周左右的时间里没有收到我们的回复,请在下方留言,以便我们注意到。我们的大部分审核都是志愿者,有时难免会有疏漏。
你可能会想要尝试以下方法来获得更快的帮助:
- 📖 查看[项目文档](https://neteasecloudmusicapienhanced.js.org) - 📖 查看[项目文档](https://neteasecloudmusicapienhanced.js.org)
- 💬 加入[QQ交流群](https://qm.qq.com/q/TpeP9Uv2yk) - 💬 加入[QQ交流群](https://qm.qq.com/q/TpeP9Uv2yk)
- 🔍 搜索[现有issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues) 看是否有类似问题 - 🔍 搜索[现有issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues) 看是否有类似问题
handle-help-wanted:
if: contains(github.event.issue.labels.*.name, 'question')
runs-on: ubuntu-latest
steps:
- name: Add help resources
uses: peter-evans/create-or-update-comment@v5
with:
issue-number: ${{ github.event.issue.number }}
body: |
🆘 需要帮助?
这里有一些可能对您有用的资源:
📚 **文档资源**
- [项目文档](https://neteasecloudmusicapienhanced.js.org)
🔍 **常见问题**
- 搜索[已关闭的issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues?q=is%3Aissue+is%3Aclosed) 看看是否有类似的问题已经被解答。
💬 **即时帮助**
- 加入QQ群:https://qm.qq.com/q/TpeP9Uv2yk
如果以上资源无法解决您的问题,请提供更多详细信息,我们会尽快为您解答!
stale-issues: stale-issues:
if: github.event.action == 'opened' if: github.event.action == 'opened'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Mark stale issues - name: Mark stale issues
uses: actions/stale@v10 uses: actions/stale@v11
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: | stale-issue-message: |
+71
View File
@@ -0,0 +1,71 @@
name: opencode
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
permissions:
contents: write
issues: write
pull-requests: write
jobs:
bot:
if: |
(contains(github.event.comment.body, ' /oc') ||
startsWith(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, ' /opencode') ||
startsWith(github.event.comment.body, '/opencode')) &&
github.actor != 'opencode-agent[bot]' &&
github.actor != 'yukka-agent[bot]' &&
github.actor != 'github-actions[bot]'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Fetch config from CF Worker
id: cfg
uses: MoeFurina/pull-oc-config@v1
with:
url: https://bot-storage.interface.focalors.ltd/config
- name: Generate bot token
id: token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ steps.cfg.outputs.app_id }}
private-key: ${{ steps.cfg.outputs.app_private_key }}
owner: ${{ github.repository_owner }}
- name: Checkout
uses: actions/checkout@v6
with:
# 需要持久化凭证: opencode action 内部会 git fetch origin <分支> 拉取 PR 代码
persist-credentials: true
token: ${{ steps.token.outputs.token }}
- name: Configure git identity
run: |
git config --global user.name "yukka-agent[bot]"
git config --global user.email "yukka-agent[bot]@users.noreply.github.com"
- name: Run opencode
uses: anomalyco/opencode/github@latest
env:
OPENCODE_API_KEY: ${{ steps.cfg.outputs.opencode_api_key }}
GITHUB_TOKEN: ${{ steps.token.outputs.token }}
OPENCODE_PERMISSION: '{"permission":{"*":"allow","question":"deny"}}'
with:
model: opencode/muse-spark-1.3-contributor-free
use_github_token: true
prompt: |
${{ steps.cfg.outputs.opencode_prompt }}
## 用户指令(来自触发评论,请严格按此执行)
${{ github.event.comment.body }}
## 用户要求Review时, 严格执行下面指令
${{ steps.cfg.outputs.pr_review_prompt }}
@@ -136,7 +136,7 @@ jobs:
esac esac
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: ${{ matrix.platform }}-binary name: ${{ matrix.platform }}-binary
path: release-artifacts/* path: release-artifacts/*
@@ -165,17 +165,16 @@ jobs:
fetch-tags: true fetch-tags: true
- name: Download build artifacts - name: Download build artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
path: release-artifacts path: release-artifacts
merge-multiple: true
- name: Flatten artifacts - name: List flattened artifacts
shell: bash shell: bash
run: | run: |
mkdir -p final-artifacts mkdir -p final-artifacts
cp release-artifacts/* final-artifacts/
find release-artifacts -type f -exec cp {} final-artifacts/ \;
ls -lah final-artifacts ls -lah final-artifacts
- name: Generate release notes - name: Generate release notes
@@ -261,13 +260,13 @@ jobs:
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GitHub Container Registry - name: Log in to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
# Step 2: run the sync action # Step 2: run the sync action
- name: Sync upstream changes - name: Sync upstream changes
id: sync id: sync
uses: aormsby/[email protected].2 uses: aormsby/[email protected].3
with: with:
upstream_sync_repo: NeteaseCloudMusicApiEnhanced/api-enhanced upstream_sync_repo: NeteaseCloudMusicApiEnhanced/api-enhanced
upstream_sync_branch: main upstream_sync_branch: main
+1
View File
@@ -10,3 +10,4 @@ anonymous_token
.vercel .vercel
.env .env
IFLOW.md IFLOW.md
precompiled
+51
View File
@@ -0,0 +1,51 @@
# NeteaseCloudMusicApiEnhanced Agent 说明
## 快速开始
- **包管理器**:开发与 CI 使用 `pnpm`,`Dockerfile` 也用 pnpm(`pnpm@9 --frozen-lockfile`,匹配 `pnpm-lock.yaml`)。仓库没有 `yarn.lock`,不要引入 yarn。
- **Node 版本**:README 推荐 Node 22+;`package.json` 的 `engines` 声明 `>=12`;CI/打包在 Node 18–24 上运行。现代 Node 均可。
- **环境变量**:`server.js` 调用了 `dotenv.config()`,本地 `.env` 会被自动加载;所有支持的变量见 `.env.prod.example`。
## 常用命令
- 安装依赖:`pnpm i`
- 启动服务:`pnpm start`(等价 `node app.js`);热重载开发:`pnpm dev`(nodemon)
- 跑测试:`pnpm test`(Mocha,超时 60s)
- Lint:`pnpm lint`;自动修复:`pnpm lint-fix`
- 文档格式化检查/修复:`pnpm docs:check` / `pnpm docs:format`
- 打包独立二进制:`pnpm pkgwin` / `pkglinux` / `pkgmacos`
## 目录内非路由文件
- `plugins/`——内部上传辅助(`upload.js`、`songUpload.js`),被 `module/*` require,本身不是路由。
- `module_example/`——接口模板与 `main.js` 库调用示例(含 `test.js` 演示 `login_cellphone`→`song_url` 链路),不是运行时代码。
- `examples/get_static_moddef.js`——用 `getModulesDefinitions(..., false)`(`doRequire=false`)把全部路由导出为 `moddef.json`。改路由注册逻辑时可跑它核对。
- `module_types/` 为空目录;类型声明在根目录 `interface.d.ts`(`types` 字段)。
## 架构
- `app.js`(也是 `bin`)——服务入口。先确保 `os.tmpdir()` 里存在 `anonymous_token`,执行 `generateConfig()` 刷新匿名 cookie 与 xeapi 公钥,再调用 `server.serveNcmApi()`。
- `server.js`——Express 工厂。`constructServer()` 自动扫描 `module/*.js`,每个文件注册一条路由(文件名 `_` 转 `/`,如 `album_new.js` → `/album/new`;特例 `daily_signin`/`fm_trash`/`personal_fm` 硬编码在 `server.js` 的 `special` 对象里)。`serveNcmApi()` 监听 `PORT`(默认 3000)/`HOST`。
- `main.js`——作为依赖被引入时的入口(`main` 字段)。把每个 `module/*` 导出为同名函数 `name(data)`,另导出 `server`、`serveNcmApi`、`getModulesDefinitions`。
- `module/*.js`——每个接口一个文件,标准写法:`module.exports = (query, request) => request(path, data, createOption(query))`。`createOption` 在 `util/option.js`,负责 crypto、cookie(回退到 `NETEASE_COOKIE`)、proxy、realIP/randomCNIP、headers、timeout。
- `util/request.js`——唯一的对外 HTTP 层(axios)。按 `crypto`(`api`/`eapi`/`weapi`/`linuxapi`/`xeapi`)加密并设置 IP 头;在 require 时同步读取 `os.tmpdir()` 里的 `anonymous_token` 与 `xeapi_public_key`。
- `util/config.json`——运行时配置:网易域名 + `APP_CONF.encrypt: true`(默认走 eapi 加密)。已被 git 跟踪,改动会改变全局默认行为。
- `index.js` / `index.mjs`——`require('./app.js')` 的薄包装,供 Vercel(`vercel.json`)和 ESM 导入使用。
- `server.js` 里有一段非显而易见的逻辑:环境变量 `ENABLE_GENERAL_UNBLOCK=true` 时,`/song/url/v1` 的响应会被自动解灰(走 `@neteasecloudmusicapienhanced/unblockmusic-utils`);另外 `query.noCookie` 为真时不向响应写 `Set-Cookie`。
## 新增/修改接口
- 新建 `module/xxx.js` 会自动挂载路由,无需注册;**文件名即路由**。
- 照抄同目录模块的写法(选对 `crypto`),用 `createOption(query)` 生成请求选项。
- 改文件名/路由会破坏已有客户端,尽量保持旧路径兼容。
## 测试
- `pnpm test` 跑 `server.test.js` + `main.test.js`。`server.test.js` 在 `before()` 里启动真实服务器,`test/*.test.js` 全部请求**真实网易云 API**——必须联网,且可能因上游风控/限流偶发失败。`main.test.js` 是纯单测,不需要网络。
- **CI 门禁只看稳定项**:`ci-check.yml` 在 Node 18/22/24 上跑 `pnpm lint` + `main.test.js` + `pnpm docs:check`;完整集成测试(`server.test.js`)是 `continue-on-error`,不阻塞合并。本地跑全量测试失败先怀疑上游风控。
- 测试使用 `power-assert`(经 `intelli-espower-loader`),普通 `assert` 写法也会输出详细 diff。
- 只跑单个用例:`pnpm exec mocha -r intelli-espower-loader -t 60000 --grep "<describe/it 名字>" server.test.js main.test.js --exit`
- 只跑离线单测:`pnpm exec mocha -r intelli-espower-loader -t 60000 main.test.js --exit`
## 坑与注意
- **改 `package.json` 的 `version` 会触发自动发布**:`release-on-version-change.yml` 按 paths 过滤——只要 push 到 `main` 时 `package.json` 有改动且 version 变了,就自动打 tag + GitHub Release(`pkg` 三平台二进制)、推送 Docker 镜像(Docker Hub + GHCR 多架构)、`pnpm publish` 到 npm;tag 已存在则跳过。别顺手改版本号,也别往 `package.json` 里写无关改动凑提交。
- **没有实际 git hooks**:`package.json` 里配了 `lint-staged` 和 husky,但 `.husky/` 下只有 `_` 脚手架目录、没有真正的 hook 文件,commit 时不会自动跑任何检查,自己记得 `pnpm lint-fix`。
- **代理环境变量已失效**:README 里关于 `http_proxy`/`https_proxy` 的警告来自旧 `request` 库时代;现在 `util/request.js` 用 axios + 自定义 keep-alive agent,且显式 `proxy: false`,环境变量代理不会生效。按请求走 `query.proxy` 参数(支持 PAC 和 http 隧道)。
- **启动令牌在系统临时目录**:`anonymous_token`、`xeapi_public_key` 存放在 `os.tmpdir()`,`util/request.js` 在 require 时同步读取。文件过期或被清空就重启服务(或调用 `generateConfig()`);首次启动先写空文件再刷新。
- **`docs:check` 校验的是 `public/docs/home.md`(docsify 站点)**,不是 README;脚本默认只处理该文件,换行会被统一为 `\n`、连续空行压缩到 2 行,PR 改动它后记得跑 `pnpm docs:format`。
- **ESLint 9 flat config**:`eslint.config.js`,风格由 `eslint-plugin-prettier` 强制(2 空格缩进、单引号、分号、`endOfLine: auto`),并 `globalIgnores(['**/public/'])`——`public/` 下的前端页面不参与 lint。
- **`pnpm-workspace.yaml` 是单包工作区**(`packages: ['.']`),并带 `allowBuilds`(core-js/es5-ext)与 `minimumReleaseAgeExclude` 白名单;改依赖安装行为时先看它。
-1418
View File
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -2,14 +2,19 @@ FROM node:lts-alpine
RUN apk add --no-cache tini RUN apk add --no-cache tini
ENV NODE_ENV production ENV NODE_ENV=production
RUN npm install -g pnpm@9
USER node USER node
WORKDIR /app WORKDIR /app
COPY --chown=node:node . ./ COPY --chown=node:node . ./
RUN yarn --network-timeout=100000 # --prod 模式下 husky 不会被安装,prepare 脚本会因找不到 husky 而失败,
# 故显式跳过生命周期脚本;本项目生产运行也不依赖任何 postinstall 步骤。
RUN pnpm install --frozen-lockfile --prod --ignore-scripts
EXPOSE 3000 EXPOSE 3000
+8 -4
View File
@@ -30,7 +30,7 @@
## 项目简介 ## 项目简介
网易云音乐第三方 Node.js API, 支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署(如果原版诈尸, 我会及时同步 or 归档)。 网易云音乐第三方 Node.js API, 支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署
> [!IMPORTANT] > [!IMPORTANT]
> >
@@ -45,14 +45,14 @@
### 环境要求 ### 环境要求
- Node.js 18 及以上 - Node.js 22 及以上
- 推荐使用 pnpm 进行依赖管理 - 推荐使用 pnpm 进行依赖管理
### 安装 ### 安装
```bash ```bash
git clone https://github.com/neteasecloudmusicapienhanced/api-enhanced.git git clone https://github.com/neteasecloudmusicapienhanced/api-enhanced.git
cd api cd api-enhanced
pnpm i pnpm i
``` ```
@@ -215,7 +215,7 @@ pnpm test
原作者 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 项目为本项目基础 (该项目在`npmjs`网站上仍持续维护, 但 github 仓库已不再更新) 原作者 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 项目为本项目基础 (该项目在`npmjs`网站上仍持续维护, 但 github 仓库已不再更新)
感谢大佬们为逆向eapi, weapi等加密算法所做的贡献 感谢大佬们为逆向eapi, weapi, xeapi等加密算法所做的贡献
项目参考: 项目参考:
@@ -225,6 +225,8 @@ pnpm test
[chaunsin/netease-cloud-music](https://github.com/chaunsin/netease-cloud-music) [chaunsin/netease-cloud-music](https://github.com/chaunsin/netease-cloud-music)
[folltoshe/netease-report-listen-song](https://github.com/folltoshe/netease-report-listen-song)
### SDK 生态 ### SDK 生态
| 语言 | 作者 | 地址 | 类型 | | 语言 | 作者 | 地址 | 类型 |
@@ -244,6 +246,8 @@ pnpm test
- [Yueby/music-together](https://github.com/Yueby/music-together) - [Yueby/music-together](https://github.com/Yueby/music-together)
- [chthollyphlie/folia-major](https://github.com/chthollyphile/folia-major)
## License ## License
[MIT License](https://github.com/MoeFurina/NeteaseCloudMusicApiEnhanced/blob/main/LICENSE) [MIT License](https://github.com/MoeFurina/NeteaseCloudMusicApiEnhanced/blob/main/LICENSE)
+14
View File
@@ -3,6 +3,8 @@ const path = require('path')
const { register_anonimous } = require('./main') const { register_anonimous } = require('./main')
const { cookieToJson, generateRandomChineseIP } = require('./util/index') const { cookieToJson, generateRandomChineseIP } = require('./util/index')
const { getXeapiPublicKey } = require('./util/xeapiKey') const { getXeapiPublicKey } = require('./util/xeapiKey')
const { loadNeapiKey } = require('./util/neapiKey')
const registerNeapikey = require('./module/register_neapikey')
const tmpPath = require('os').tmpdir() const tmpPath = require('os').tmpdir()
async function generateConfig() { async function generateConfig() {
@@ -37,5 +39,17 @@ async function generateConfig() {
} catch (error) { } catch (error) {
console.log(error) console.log(error)
} }
try {
const current = loadNeapiKey()
await registerNeapikey(
{
version: current ? current.version : 0,
behavior: current ? 'update' : 'open',
},
null,
)
} catch (error) {
console.log(error)
}
} }
module.exports = generateConfig module.exports = generateConfig
+1074
View File
File diff suppressed because it is too large Load Diff
+51
View File
@@ -0,0 +1,51 @@
// 获取广告
const createOption = require('../util/option.js')
module.exports = async (query, request) => {
const data = {
type_ids: query.type_ids || '["400002_0"]',
}
const option = createOption(query, 'xeapi', 'v3')
const res = await request(`/api/ad/get`, data, option)
const raw = res.body
// 提取广告中的 req_id
let reqId = ''
try {
if (raw?.ads) {
const ad = Object.values(raw.ads)[0]
// 逆向 v9.5.61:客户端从 ad.adExtMap["req_id"] 取 reqUid
if (ad?.adExtMap) {
if (typeof ad.adExtMap === 'string') {
try {
reqId = JSON.parse(ad.adExtMap).req_id || ''
} catch (_) {}
} else {
reqId = ad.adExtMap.req_id || ''
}
}
// 兜底:adLogId.requestId / ad.reqId / extJson.contextInfo.req_id
if (!reqId && ad?.adLogId?.requestId) reqId = ad.adLogId.requestId
if (!reqId && ad?.reqId) reqId = ad.reqId
if (!reqId && ad?.extJson) {
try {
const ext = JSON.parse(ad.extJson)
reqId = ext?.contextInfo?.req_id || ''
} catch (_) {}
}
}
} catch (_) {}
return {
status: 200,
body: {
code: 200,
ads: raw?.ads || null,
message: raw?.message || null,
extra: { reqId },
},
}
}
+13
View File
@@ -0,0 +1,13 @@
// 获取免费听时长状态
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
entrance: 'FREE_LISTEN_RN',
}
return request(
`/api/ad/homepage/free/tab/extend/v2`,
data,
createOption(query, 'xeapi'),
)
}
+252
View File
@@ -0,0 +1,252 @@
// 看广告免费听歌 - 领取免费听权益
// 请求流程(基于逆向网易云音乐 v9.5.61 原生 Kotlin 源码,classes5/18/19.dex):
// 1. 从广告平台拉广告 → 用户看完/点击广告 → 获取 ad 对象的 extJson.contextInfo.req_id 作为 reqUid
// 2. 调用本接口传入 reqUid 及相关权益参数,领取权益
// 3. 服务器返回 gainFlag 等标识,用于展示领取结果
//
// 调用链(逆向还原):
// AdDSLIncentiveVideoRightsHelper.requestRightsGainInner
// → AdDSLUtils.requestRightGain(ad, exposeTime, clickTime, creativeType, cb)
// → 构造 ListeningRightRequestParams(21 字段)→ JSON.stringify
// → body = { "reqParam": "..." } → POST /api/ad/listening/rights/gain
// (注意:客户端 AdDSLIncentiveVideoRightsHelper 写死 creativeType=36)
//
// 接口不只领取听歌时长,还包含"看视频得云贝"等权益:
// rightsGainMethod=6 (LAXIN_EXPOSE_OR_DOWNLOAD 拉新曝光/下载分段权益) 时,
// 弹窗 AdLaxinSegmentedGuideDialog 展示 "获得X云贝"(2000 云贝等),
// 权益数值由广告下发的 AdLaxinSegmentedRightsGuidePopup.rightsValue 决定,
// extraRightsType 作为权益类型随本接口上传。
//
// rightsGainMethod 枚举(h80/a):
// 1=EXPOSE 曝光, 2=EXPOSE_CLICK 曝光+点击, 3=EXPOSE_DOWNLOAD 曝光+下载,
// 4=CLICK_STAY 点击+停留, 5=EXPOSURE_OR_CLICK_STAY 曝光或点击停留,
// 6=LAXIN_EXPOSE_OR_DOWNLOAD 拉新曝光/下载分段权益
const createOption = require('../util/option.js')
const adGet = require('./ad_get.js')
// 安全 JSON 解析(字符串字段可能是 JSON 文本)
function safeParse(str, fallback) {
if (typeof str !== 'string') return fallback
try {
return JSON.parse(str)
} catch (_) {
return fallback
}
}
module.exports = async (query, request) => {
const time = Date.now()
// 从广告对象自动补齐请求字段。
// 实测 v9.5.61 真实 API 返回(/ad/get)确认的字段路径:
// reqUid = ad.extJson.contextInfo.req_id(真实响应中唯一存在)
// contextInfo = ad.extJson.contextInfo(完整对象序列化)
// generalRightsInfo = ad.generalRightsInfo(字符串 JSON)
// creativeType = ad.creativeType
// rightsGainMethod / extraRightsType / rightsGainDuration / rightsGainType /
// extraRightsGainMethod / extraRightsGainDuration / nextRightsGainDuration /
// rightsExtJson / source / rightsUpperLimit / qualified
// = ad.generalRightsInfo(字符串)解析后取值
// (逆向类字段 adExtMap/adLogId/listeningRightHintInfo 真实响应中不存在,仅兜底)
// sniffTime = method==3 或 6 时 currentTimeMillis
let reqUid = query.reqUid || ''
let contextInfo = query.contextInfo
let creativeType = query.creativeType
let generalRightsInfo = query.generalRightsInfo
// 广告 hint 配置:仅当调用方未显式传参时用广告下发值兜底
const hint = {}
if (!reqUid || contextInfo === undefined || creativeType === undefined) {
try {
const adRes = await adGet(
{ ...query, type_ids: query.type_ids || '["400002_0"]' },
request,
)
const ad = Object.values(adRes?.body?.ads || {})[0]
if (!ad) throw new Error('ads 为空(未登录或广告位无广告)')
// reqUid:真实 API 返回中 req_id 在 ad.extJson.contextInfo.req_id(实测 v9.5.61)
// (逆向类字段 adExtMap/adLogId 在真实响应中不存在,仅作兜底)
if (!reqUid) {
const ext =
typeof ad?.extJson === 'string'
? safeParse(ad.extJson, {})
: ad?.extJson || {}
const extMap =
typeof ad?.adExtMap === 'string'
? safeParse(ad.adExtMap, {})
: ad?.adExtMap
reqUid =
ext?.contextInfo?.req_id ||
extMap?.req_id ||
ad?.adLogId?.requestId ||
ad?.reqId ||
adRes?.body?.extra?.reqId ||
''
}
// contextInfo:真实 API 返回中在 ad.extJson.contextInfo(实测 v9.5.61)
if (contextInfo === undefined) {
const ext =
typeof ad?.extJson === 'string'
? safeParse(ad.extJson, {})
: ad?.extJson || {}
const ci =
ext?.contextInfo || ad?.adLogId?.contextInfo || ad?.showContext
if (ci) {
contextInfo = typeof ci === 'string' ? ci : JSON.stringify(ci)
}
}
if (creativeType === undefined && ad?.creativeType !== undefined) {
creativeType = ad.creativeType
}
if (generalRightsInfo === undefined && ad?.generalRightsInfo) {
generalRightsInfo =
typeof ad.generalRightsInfo === 'string'
? ad.generalRightsInfo
: JSON.stringify(ad.generalRightsInfo)
}
// 缓存广告下发的领取配置(后续用于兜底)
// 实测 v9.5.61:rightsGainMethod/rightsUpperLimit/qualified 等在
// ad.generalRightsInfo(字符串)里,ad.listeningRightHintInfo 真实响应中不存在
const hintCfg =
typeof ad?.listeningRightHintInfo === 'string'
? safeParse(ad.listeningRightHintInfo, {})
: ad?.listeningRightHintInfo || {}
const gri =
typeof ad?.generalRightsInfo === 'string'
? safeParse(ad.generalRightsInfo, {})
: ad?.generalRightsInfo || {}
Object.assign(hint, hintCfg, gri)
console.log(`自动获取 reqUid: ${reqUid}`)
} catch (e) {
// 获取广告失败,后续请求会因缺少 reqUid 被拒绝
}
}
const rightsGainMethod = query.rightsGainMethod
? parseInt(query.rightsGainMethod)
: hint.rightsGainMethod || 2
const rightsParam = {
// 必填: 广告请求 ID,自动从 ad_get 获取
reqUid,
// 曝光时间戳
exposureTime: query.exposureTime ? parseInt(query.exposureTime) : time,
// 当前登录用户 ID(原版客户端从 Profile.getUserId() 获取)
userId: query.uid ? parseInt(query.uid) : undefined,
// 点击时间戳
clickTime: query.clickTime ? parseInt(query.clickTime) : time,
// 额外权益类型(拉新分段权益等,决定发放云贝/时长/下载,来自广告配置)
extraRightsType: query.extraRightsType
? parseInt(query.extraRightsType)
: hint.extraRightsType !== undefined
? parseInt(hint.extraRightsType)
: undefined,
// 是否连续播放(默认 false)
playContinuously: query.playContinuously ? true : false,
// 来源标识(原版从 ad.listeningRightHintInfo.source 读取)
source: query.source
? parseInt(query.source)
: hint.source !== undefined
? parseInt(hint.source)
: undefined,
// 广告创意类型(激励视频场景=36,优先取 query / 广告对象)
creativeType: creativeType !== undefined ? parseInt(creativeType) : 36,
// 权益领取方式(1~6 枚举,见文件头注释)
rightsGainMethod,
// 权益扩展方式与时长
extraRightsGainMethod: query.extraRightsGainMethod
? parseInt(query.extraRightsGainMethod)
: hint.extraRightsGainMethod !== undefined
? parseInt(hint.extraRightsGainMethod)
: undefined,
extraRightsGainDuration: query.extraRightsGainDuration
? parseInt(query.extraRightsGainDuration)
: hint.extraRightsGainDuration !== undefined
? parseInt(hint.extraRightsGainDuration)
: undefined,
nextRightsGainDuration: query.nextRightsGainDuration
? parseInt(query.nextRightsGainDuration)
: hint.nextRightsGainDuration !== undefined
? parseInt(hint.nextRightsGainDuration)
: undefined,
// 权益类型(含 RIGHTS_GAIN_TYPE_CURRENT_DAY 等取值)
rightsGainType: query.rightsGainType
? parseInt(query.rightsGainType)
: hint.rightsGainType !== undefined
? parseInt(hint.rightsGainType)
: undefined,
// 权益时长
rightsGainDuration: query.rightsGainDuration
? parseInt(query.rightsGainDuration)
: hint.rightsGainDuration !== undefined
? parseInt(hint.rightsGainDuration)
: undefined,
// 领取步骤(UNGAIN/GAINING/GAIN_FINISHED 对应的值)
gainMethodStep: query.gainMethodStep
? parseInt(query.gainMethodStep)
: undefined,
// 通用权益信息(ad.generalRightsInfo 序列化 JSON)
generalRightsInfo,
// 权益扩展信息
rightsExtJson: query.rightsExtJson || hint.rightsExtJson || undefined,
// 应用信息(下载类广告)
appInfo: query.appInfo ? JSON.parse(query.appInfo) : undefined,
// 广告上下文(ad.adLogId.contextInfo,客户端真实来源)
contextInfo,
// 应用是否已安装(下载类广告)
installed: query.installed ? parseInt(query.installed) : undefined,
// 嗅探时间:逆向确认(classes5.dex AdDSLUtils.requestRightGain):
// rightsGainMethod==3(曝光+下载) 或 6(LAXIN) 时传 currentTimeMillis
sniffTime:
rightsGainMethod === 3 || rightsGainMethod === 6
? query.sniffTime
? parseInt(query.sniffTime)
: time
: undefined,
}
// 清理 undefined 字段
Object.keys(rightsParam).forEach((key) => {
if (rightsParam[key] === undefined) delete rightsParam[key]
})
// 将参数序列化为 reqParam 字符串 (与原生源码一致)
const data = {
reqParam: JSON.stringify(rightsParam),
}
const res = await request(
`/api/ad/listening/rights/gain`,
data,
createOption(query, 'xeapi', 'v3'),
)
return {
status: 200,
body: {
code: 200,
data: res.body,
},
}
}
+16
View File
@@ -0,0 +1,16 @@
// 获取关注歌手的新歌曲和 MV
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
startTimestamp: query.startTimestamp || query.before || Date.now(),
sourceType: query.sourceType || 1,
limit: query.limit || 10,
firstRequest: query.firstRequest ?? true,
}
return request(
`/api/sub/artist/new/works/song-mv/list/v2`,
data,
createOption(query, 'eapi'),
)
}
+10
View File
@@ -0,0 +1,10 @@
// 获取所有关注歌手最近的 50 首新歌
const createOption = require('../util/option.js')
module.exports = (query, request) => {
return request(
`/api/sub/artist/new/works/song/playall`,
{},
createOption(query, 'eapi'),
)
}
+13
View File
@@ -0,0 +1,13 @@
// 发送安全验证码
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
ctcode: query.ctcode || '86',
}
return request(
`/api/sms/captcha/safe/sent`,
data,
createOption(query, 'eapi'),
)
}
+16
View File
@@ -0,0 +1,16 @@
// 发送验证码
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
ctcode: query.ctcode || '86',
secrete: 'music_middleuser_pclogin',
cellphone: query.phone,
scene: '0',
}
return request(
`/api/middle/captcha/sent/v1`,
data,
createOption(query, 'eapi'),
)
}
+5 -1
View File
@@ -6,5 +6,9 @@ module.exports = (query, request) => {
cellphone: query.phone, cellphone: query.phone,
countrycode: query.countrycode, countrycode: query.countrycode,
} }
return request(`/api/cellphone/existence/check`, data, createOption(query)) return request(
`/api/cellphone/existence/check`,
data,
createOption(query, 'eapi'),
)
} }
+1 -1
View File
@@ -25,6 +25,6 @@ module.exports = (query, request) => {
return request( return request(
`/api/resource/comments/${query.t}`, `/api/resource/comments/${query.t}`,
data, data,
createOption(query, 'weapi'), createOption(query, 'eapi', 'v2'),
) )
} }
+18
View File
@@ -0,0 +1,18 @@
const { resourceTypeMap } = require('../util/config.json')
// 发送评论
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
threadId: resourceTypeMap[query.type] + query.id,
content: query.content,
resourceType: '0',
expressionPicId: '-1',
bubbleId: '-1',
}
return request(
`/api/resource/comments/add`,
data,
createOption(query, 'xeapi', 'v3'),
)
}
+15
View File
@@ -0,0 +1,15 @@
const { resourceTypeMap } = require('../util/config.json')
// 删除评论
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
commentId: query.cid,
threadId: resourceTypeMap[query.type] + query.id,
}
return request(
`/api/resource/comments/delete`,
data,
createOption(query, 'xeapi'),
)
}
+17
View File
@@ -0,0 +1,17 @@
const { resourceTypeMap } = require('../util/config.json')
// 发送评论
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
threadId: resourceTypeMap[query.type] + query.id,
commentId: query.cid,
content: query.content,
resourceType: '0',
}
return request(
`/api/v1/resource/comments/reply`,
data,
createOption(query, 'xeapi', 'v3'),
)
}
+1 -1
View File
@@ -48,7 +48,7 @@ module.exports = async (query, request) => {
case 'linuxapi': { case 'linuxapi': {
if (isReq) { if (isReq) {
const pureHex = data.replace(/\s/g, '') const pureHex = data.replace(/\s/g, '')
const decrypted = aesDecrypt(pureHex, linuxapiKey, '', 'hex') const decrypted = aesDecrypt(pureHex, 'ecb', linuxapiKey, '', 'hex')
result = JSON.parse(decrypted.toString(CryptoJS.enc.Utf8)) result = JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
} else { } else {
result = typeof data === 'string' ? JSON.parse(data) : data result = typeof data === 'string' ? JSON.parse(data) : data
+14
View File
@@ -0,0 +1,14 @@
// 强制下线设备
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
key: query.deviceKey,
captcha: query.captcha || '',
}
return request(
`/api/middle/user/security/device/kickoff`,
data,
createOption(query, 'eapi'),
)
}
+13
View File
@@ -0,0 +1,13 @@
// 登录设备列表
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
excStatus: '9',
}
return request(
`/api/middle/user/device/list`,
data,
createOption(query, 'eapi'),
)
}
+28
View File
@@ -0,0 +1,28 @@
// 上报设备中心设备名称
// 官方 PC 端在登录成功后调用此接口,将当前登录设备在“登录设备管理”中的名称设置为自定义名称
const createOption = require('../util/option.js')
module.exports = async (query, request) => {
const deviceName = String(query.deviceName || query.name || '').trim()
if (!deviceName) {
return Promise.reject({
status: 400,
body: {
code: 400,
msg: '缺少必要参数: deviceName',
},
})
}
const data = {
deviceName,
}
return request(
`/api/deviceinfo/center/upload`,
data,
createOption(query, 'eapi'),
)
}
+34
View File
@@ -0,0 +1,34 @@
// 修改本人动态的可见权限
const createOption = require('../util/option.js')
const PRIVACY_VALUES = new Set([0, 1, 2, 6])
module.exports = (query, request) => {
const eventId = String(query.evId ?? '').trim()
const rawPrivacy = String(query.privacy ?? '').trim()
const privacy = Number(rawPrivacy)
if (
!eventId ||
!rawPrivacy ||
!Number.isInteger(privacy) ||
!PRIVACY_VALUES.has(privacy)
) {
return Promise.resolve({
status: 400,
body: {
code: 400,
message: 'evId is required and privacy must be one of 0, 1, 2, 6',
},
cookie: [],
})
}
const data = {
eventId,
privacy,
}
return request(`/api/event/privacy/op`, data, createOption(query))
}
+15
View File
@@ -0,0 +1,15 @@
// 乐迷团详情
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
groupId: query.groupId || query.id || '',
scene: query.scene || '',
}
return request(
'/api/social/fansgroup/bff/detail/get',
data,
createOption(query, 'eapi'),
)
}
+17
View File
@@ -0,0 +1,17 @@
// 乐迷团推荐笔记
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
artistSelf: query.artistSelf || '0',
fansGroupId: query.fansGroupId || query.groupId || '',
cursor: query.cursor || '0',
size: String(query.size || 10),
}
return request(
'/api/fans/group/feed/recommend/get',
data,
createOption(query, 'eapi'),
)
}
+14
View File
@@ -0,0 +1,14 @@
// 用户所处乐迷团详情
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
groupId: query.groupId || query.id || '',
}
return request(
'/api/social/fansgroup/bff/user/group/detail/get',
data,
createOption(query, 'eapi'),
)
}
+11
View File
@@ -0,0 +1,11 @@
// 获取当前登录用户加入的全部歌手乐迷团列表
const createOption = require('../util/option.js')
module.exports = (query, request) => {
return request(
'/api/social/fansgroup/bff/user/groups/get',
{},
createOption(query, 'eapi'),
)
}
+13
View File
@@ -0,0 +1,13 @@
// 红心与取消红心歌曲- v1
const createOption = require('../util/option.js')
module.exports = (query, request) => {
query.like = query.like == 'false' ? false : true
const data = {
alg: 'itembased',
trackId: query.id,
like: query.like,
time: '3',
}
return request(`/api/v1/radio/like`, data, createOption(query, 'xeapi', 'v3'))
}
+12
View File
@@ -0,0 +1,12 @@
// 听歌足迹 - 歌曲播放排行 (Top20)
const createOption = require('../util/option.js')
module.exports = (query, request) => {
return request(
`/api/content/activity/listen/data/song/play/rank`,
{
type: query.type || 'month', //周 week 月 month
endTime: query.endTime, // 不填就是本周/月的
},
createOption(query),
)
}
+1
View File
@@ -14,6 +14,7 @@ module.exports = async (query, request) => {
? query.captcha ? query.captcha
: query.md5_password || CryptoJS.MD5(query.password).toString(), : query.md5_password || CryptoJS.MD5(query.password).toString(),
remember: 'true', remember: 'true',
secureCaptcha: query.sca || '',
} }
let result = await request( let result = await request(
`/api/w/login/cellphone`, `/api/w/login/cellphone`,
+24
View File
@@ -0,0 +1,24 @@
// 云小编每日抽奖
//
// activityId:
// 默认 6501202
//
// drawCount:
// 默认 1
//
// checkToken:
// 易盾反作弊 Token
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
activityId: query.activityId || '6501202',
drawCount: query.drawCount || '1',
}
return request(
`/api/middle/play/do/lottery`,
data,
createOption(query, 'eapi', 'v2'),
)
}
@@ -0,0 +1,17 @@
// 云小编抽奖剩余次数查询
//
// activityId:
// 默认 6501202
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
activityId: query.activityId || '6501202',
}
return request(
`/api/middle/play/lottery/remain/chance`,
data,
createOption(query, 'eapi'),
)
}
-3
View File
@@ -1,8 +1,5 @@
// 私人FM - 模式选择 // 私人FM - 模式选择
// aidj, DEFAULT, FAMILIAR, EXPLORE, SCENE_RCMD ( EXERCISE, FOCUS, NIGHT_EMO )
// 来不及解释这几个了
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
module.exports = (query, request) => { module.exports = (query, request) => {
const data = { const data = {
+1
View File
@@ -1,4 +1,5 @@
// 私信和通知接口 // 私信和通知接口
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
module.exports = (query, request) => { module.exports = (query, request) => {
const data = {} const data = {}
+1 -1
View File
@@ -9,6 +9,6 @@ module.exports = (query, request) => {
? { checkToken: query.checkToken || APP_CONF.checkToken } ? { checkToken: query.checkToken || APP_CONF.checkToken }
: {}), : {}),
} }
query.checkToken = true // 强制开启checkToken query.checkToken = 'v2' // 强制开启checkToken
return request(`/api/playlist/${path}`, data, createOption(query, 'eapi')) return request(`/api/playlist/${path}`, data, createOption(query, 'eapi'))
} }
+11
View File
@@ -0,0 +1,11 @@
// 每日风格推荐场景配置列表获取
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {}
return request(
`/api/homepage/daily/song/config/get`,
data,
createOption(query, 'xeapi'),
)
}
+17
View File
@@ -0,0 +1,17 @@
// 每日风格推荐场景配置设置
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
tags: JSON.stringify({
tagIds: String(query.tags).split(',').map(Number),
categoryId: Number(query.category),
}),
}
console.log(data)
return request(
`/api/homepage/daily/song/tag/save`,
data,
createOption(query, 'xeapi'),
)
}
+11
View File
@@ -0,0 +1,11 @@
// 每日推荐风格歌曲
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {}
return request(
`/api/homepage/category/daily/song/list`,
data,
createOption(query, 'xeapi'),
)
}
+13
View File
@@ -0,0 +1,13 @@
// 每日推荐歌曲 - v1
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
ispush: query.ispush || false,
}
return request(
`/api/v3/discovery/recommend/songs`,
data,
createOption(query, 'xeapi'),
)
}
+3 -1
View File
@@ -1,3 +1,5 @@
// 获取游客cookie
const CryptoJS = require('crypto-js') const CryptoJS = require('crypto-js')
const path = require('path') const path = require('path')
const fs = require('fs') const fs = require('fs')
@@ -37,7 +39,7 @@ module.exports = async (query, request) => {
let result = await request( let result = await request(
`/api/register/anonimous`, `/api/register/anonimous`,
data, data,
createOption(query, 'weapi'), createOption(query, 'xeapi'),
) )
if (result.body.code === 200) { if (result.body.code === 200) {
result = { result = {
+158
View File
@@ -0,0 +1,158 @@
// 易盾反作弊 Token 注册端点
// 通过易盾官方 Watchman SDK(Web 版,跑在 jsdom 模拟的浏览器环境里)
// 实时调用 getToken(businessId) 获取反作弊 token,供后续带 checkToken 的请求使用
//
// GET /register/checktoken/v2 → 实时获取新 token(不缓存)
// POST /register/checktoken/v2 → 实时获取新 token
//
// 注意:每次获取都不缓存,模拟真实客户端每次请求使用新鲜 token,
// 避免反作弊 token 复用触发风控。
//
// 注:jsdom 固定用 v24(engines >=18),其依赖链为纯 CJS,可被 pkg 静态
// 分析自动打包;v30+ 引入纯 ESM 依赖且要求 Node >=22,无法在 CI(18-24) 与
// pkg(node18) 目标下运行。
//
const { JSDOM, VirtualConsole } = require('jsdom')
const { default: axios } = require('axios')
const { APP_CONF } = require('../util/config.json')
const logger = require('../util/logger')
// 网易云音乐在易盾的 productNumber 与 businessId
const PRODUCT_NUMBER = 'YD00000558929251'
const BUSINESS_ID = 'bd5d2f973ef74cd2a61325a412ae54d9'
const TOOL_JS_URL = `${APP_CONF.dunStaticDomain}/tool.min.js`
// 最小 HTML 外壳,模拟网页环境
const HTML =
'<!doctype html><html><head><meta charset="UTF-8"></head><body></body></html>'
let toolJs = ''
let wm = null // Watchman 实例(进程内复用,可反复 getToken)
let dom = null // jsdom 实例(失败时 close 释放活动句柄,避免泄漏)
let initPromise = null
// 获取 tool.min.js(内存缓存,避免每次初始化重复下载)
async function getToolJs() {
if (toolJs) return toolJs
const res = await axios.get(TOOL_JS_URL, { timeout: 10000 })
toolJs = String(res.data)
return toolJs
}
// 初始化 Watchman(进程内只初始化一次,实例可反复 getToken)
async function ensureWatchman() {
if (wm) return wm
if (initPromise) return initPromise
initPromise = (async () => {
const js = await getToolJs()
const virtualConsole = new VirtualConsole()
virtualConsole.on('jsdomError', () => {})
dom = new JSDOM(HTML, {
url: 'https://music.163.com/',
referrer: 'https://music.163.com/',
contentType: 'text/html',
runScripts: 'dangerously',
resources: 'usable', // 允许动态加载 watchman.min.js / JSONP
pretendToBeVisual: true,
virtualConsole,
beforeParse(window) {
// 抹掉 headless 特征,避免易盾风控误判
Object.defineProperty(window.navigator, 'webdriver', {
get: () => undefined,
})
window.chrome = { runtime: {} }
window.navigator.languages = ['zh-CN', 'zh']
window.navigator.plugins = [1, 2, 3, 4, 5]
},
})
const script = dom.window.document.createElement('script')
script.textContent = js
dom.window.document.body.appendChild(script)
return new Promise((resolve, reject) => {
let settled = false
const timer = setTimeout(() => {
if (settled) return
settled = true
reject(new Error('watchman 初始化超时'))
}, 15000)
dom.window.initWatchman({
auto: true,
productNumber: PRODUCT_NUMBER,
onload(instance) {
if (settled) return
settled = true
clearTimeout(timer)
wm = instance
resolve(instance)
},
onerror(...args) {
if (settled) return
settled = true
clearTimeout(timer)
reject(new Error('watchman 初始化失败'))
},
})
})
})()
try {
return await initPromise
} catch (e) {
// 失败路径:关闭 jsdom 释放 rAF/子资源/定时器等活动句柄,防止每次失败泄漏约 30MB
if (dom) {
dom.window.close()
dom = null
}
initPromise = null
wm = null
throw e
}
}
// 获取新 token
async function fetchToken() {
const instance = await ensureWatchman()
const raw = instance.getInstance()
await new Promise((resolve) => {
const timer = setTimeout(() => resolve(), 15000)
raw.I(() => {
clearTimeout(timer)
resolve()
})
})
return new Promise((resolve) => {
const timer = setTimeout(() => resolve(''), 15000)
instance.getToken(BUSINESS_ID, (tk) => {
clearTimeout(timer)
resolve(typeof tk === 'string' ? tk : '')
})
})
}
// 端点处理:每次实时获取新 token
module.exports = async () => {
let token = ''
try {
token = await fetchToken()
} catch (e) {
logger.warn('[checkToken v2]', e.message)
}
return {
status: 200,
body: { code: 200, token, registered: !!token },
}
}
// 给 request.js 读取用:每次调用实时获取新 token,不缓存
module.exports.getToken = async () => {
try {
return await fetchToken()
} catch (e) {
logger.warn('[checkToken v2]', e.message)
return ''
}
}
+44
View File
@@ -0,0 +1,44 @@
// 易盾反作弊 Token 注册端点
// 调用后获取实时 token 并存入共享存储,供后续带 checkToken 的请求使用
//
// GET /register/checktoken/v3 → 实时获取新 token(不缓存)
// POST /register/checktoken/v3 → 实时获取新 token
//
// 注意:每次获取都不缓存,模拟真实客户端每次请求使用新鲜 token,
// 避免反作弊 token 复用触发风控。
//
const { default: axios } = require('axios')
const { APP_CONF } = require('../util/config.json')
const URL = APP_CONF.dunDomainV3 + '/v3/b?pn=YD00000558929251'
async function fetch() {
const res = await axios.get(URL, { timeout: 10000 })
const body = String(res.data)
const m = body.match(/null\(\[(\d+),\d+,\"([^\"]+)\"\]\)/)
if (m && m[1] === '200') return m[2]
throw new Error('易盾返回异常: ' + body.substring(0, 100))
}
// 端点处理:每次实时获取新 token
module.exports = async () => {
let token = ''
try {
token = await fetch()
} catch (e) {
// token 获取失败时返回空,由调用方决定是否重试
}
return {
status: 200,
body: { code: 200, token, registered: !!token },
}
}
// 给 request.js 读取用:每次调用实时获取新 token,不缓存
module.exports.getToken = async () => {
try {
return await fetch()
} catch (e) {
return ''
}
}
+48
View File
@@ -0,0 +1,48 @@
const { default: axios } = require('axios')
const { APP_CONF } = require('../util/config.json')
const { saveNeapiKey } = require('../util/neapiKey')
const { decodeConfig } = require('../util/neapiConfig')
const USER_AGENT =
'NeteaseMusic/9.5.61.260802021928(9005061);Dalvik/2.1.0 (Linux; U; Android 12; HBN-AL00 Build/cd737a2.0)'
module.exports = async (query, request) => {
const version = Number(query.version) || 0
const behavior = query.behavior === 'update' ? 'update' : 'open'
const res = await axios({
method: 'GET',
url: APP_CONF.apiDomain + '/api/gorilla/algorithm/record',
params: { version, behavior },
headers: { 'User-Agent': USER_AGENT },
proxy: false,
})
if (!res.data || res.data.code !== 200 || !res.data.data) {
throw new Error('neapi config request failed')
}
// file 为空表示本地已是最新,无需更新
const { file, circleTime } = res.data.data
const config = file
? saveNeapiKey(decodeConfig(Buffer.from(file, 'base64')), circleTime)
: null
// 只回版本与密钥偏移,密钥本身留在本地缓存
const keyOffsets = {}
if (config) {
keyOffsets.encrypt = config.keyBlob.indexOf(config.encryptKey)
keyOffsets.decrypt = config.keyBlob.indexOf(config.decryptKey)
}
return {
status: 200,
body: {
version: config ? config.version : res.data.data.version,
updated: Boolean(config),
circleTime,
keyOffsets,
},
cookie: [],
}
}
+74
View File
@@ -0,0 +1,74 @@
const { default: axios } = require('axios')
const encrypt = require('../util/crypto')
const { APP_CONF } = require('../util/config.json')
const generateNonce = () => {
let nonce = ''
for (let i = 0; i < 16; i++) {
nonce += Math.floor(Math.random() * 10).toString()
}
return nonce
}
module.exports = async (query, request) => {
const nonce = generateNonce()
const timestamp = String(Date.now())
const deviceId = query.deviceId || global.deviceId || ''
const currentKeyVersion = query.currentKeyVersion || ''
const data = {
appVersion: '9.5.61',
currentKeyVersion,
deviceId,
nonce,
os: 'android',
requestType: 'active',
signature: encrypt.xeapiSign(timestamp, nonce),
t1: '',
t2: '',
timestamp,
uid: '',
}
const res = await axios({
method: 'POST',
url: APP_CONF.apiDomain + '/api/bsr/sk/get',
headers: {
'User-Agent':
'NeteaseMusic/9.5.61.260802021928(9005061);Dalvik/2.1.0 (Linux; U; Android 12; HBN-AL00 Build/cd737a2.0)',
Cookie: deviceId ? `deviceId=${encodeURIComponent(deviceId)}` : '',
},
data: new URLSearchParams(data).toString(),
proxy: false,
})
if (
!res.data ||
res.data.code !== 200 ||
!res.data.data ||
!res.data.data.encryptedData
) {
throw new Error('xeapi public key request failed')
}
if (
!res.data.data.signature ||
encrypt.xeapiSign(res.data.data.timestamp, nonce) !==
res.data.data.signature
) {
throw new Error('xeapi public key response signature mismatch')
}
const publicKey = encrypt.xeapiDecryptPublicKey(res.data.data.encryptedData)
if (!publicKey.sk) {
throw new Error('xeapi public key response missing sk')
}
return {
status: 200,
body: {
...publicKey,
deviceId,
},
cookie: [],
}
}
+49
View File
@@ -0,0 +1,49 @@
// 提交歌曲播放状态
const createOption = require('../util/option.js')
const generateSessionId = () =>
Array.from(
{ length: 12 },
() =>
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'[Math.floor(Math.random() * 36)],
).join('')
module.exports = (query, request) => {
const {
id,
sessionId,
progress = 0,
playMode = 'list_loop',
type = 'song',
} = query
if (!id) {
return Promise.reject({
status: 400,
body: {
code: 400,
msg: '缺少必要参数:id',
},
})
}
const playStateSubmitReq = JSON.stringify({
resource: {
id: String(id),
type: type,
},
progress: Number(progress) || 0,
sessionId: sessionId || generateSessionId(),
playMode: playMode,
})
const data = {
playStateSubmitReq: playStateSubmitReq,
}
return request(
'/api/relay/play/state/submit',
data,
createOption(query, 'weapi'),
)
}
+17
View File
@@ -0,0 +1,17 @@
// 云小编领取任务积分
//
// activityId:
// 调用 rep/ugc/activity/get 获取
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
activityId: query.activityId || '5001',
}
return request(
`/api/rep/ugc/activity/collect`,
data,
createOption(query, 'eapi'),
)
}
+7
View File
@@ -0,0 +1,7 @@
// 云小编活动信息
const createOption = require('../util/option.js')
module.exports = (query, request) => {
return request(`/api/rep/ugc/activity/get`, {}, createOption(query, 'eapi'))
}
+28
View File
@@ -0,0 +1,28 @@
// 云小编考试状态
//
// examType:
// 1. 歌曲曲风审核: musicalStyleEnter
// 2. 歌曲语种审核: languageEnter
// 3. 歌曲原唱审核: oriSingerEnter
// 4. 情绪标签审核: emotionEnter
const createOption = require('../util/option.js')
module.exports = (query, request) => {
if (!query.examType)
return Promise.reject({
status: 400,
body: {
code: 400,
message: '参数不足',
},
})
const data = {
examType: query.examType,
}
return request(
'/api/rep/ugc/exam/info/get',
data,
createOption(query, 'eapi'),
)
}
@@ -0,0 +1,32 @@
// 云小编考试取题
//
// examType:
// 1. 歌曲曲风审核: musicalStyleEnter
// 2. 歌曲语种审核: languageEnter
// 3. 歌曲原唱审核: oriSingerEnter
// 4. 情绪标签审核: emotionEnter
//
// taskId:
// 首次调用 rep/ugc/exam/start 获取,之后调用 rep/ugc/exam/info/get 获取
const createOption = require('../util/option.js')
module.exports = (query, request) => {
if (!query.examType || !query.taskId)
return Promise.reject({
status: 400,
body: {
code: 400,
message: '参数不足',
},
})
const data = {
examType: query.examType,
taskId: query.taskId,
}
return request(
'/api/rep/ugc/exam/question/single/get',
data,
createOption(query, 'eapi'),
)
}
+32
View File
@@ -0,0 +1,32 @@
// 云小编考试结果
//
// examType:
// 1. 歌曲曲风审核: musicalStyleEnter
// 2. 歌曲语种审核: languageEnter
// 3. 歌曲原唱审核: oriSingerEnter
// 4. 情绪标签审核: emotionEnter
//
// taskId:
// 首次调用 rep/ugc/exam/start 获取,之后调用 rep/ugc/exam/info/get 获取
const createOption = require('../util/option.js')
module.exports = (query, request) => {
if (!query.examType || !query.taskId)
return Promise.reject({
status: 400,
body: {
code: 400,
message: '参数不足',
},
})
const data = {
examType: query.examType,
taskId: query.taskId,
}
return request(
'/api/rep/ugc/exam/result/get',
data,
createOption(query, 'eapi'),
)
}
+24
View File
@@ -0,0 +1,24 @@
// 云小编考试开始
//
// examType:
// 1. 歌曲曲风审核: musicalStyleEnter
// 2. 歌曲语种审核: languageEnter
// 3. 歌曲原唱审核: oriSingerEnter
// 4. 情绪标签审核: emotionEnter
const createOption = require('../util/option.js')
module.exports = (query, request) => {
if (!query.examType)
return Promise.reject({
status: 400,
body: {
code: 400,
message: '参数不足',
},
})
const data = {
examType: query.examType,
}
return request('/api/rep/ugc/exam/start', data, createOption(query, 'eapi'))
}
+36
View File
@@ -0,0 +1,36 @@
// 云小编考试提交
//
// examType:
// 1. 歌曲曲风审核: musicalStyleEnter
// 2. 歌曲语种审核: languageEnter
// 3. 歌曲原唱审核: oriSingerEnter
// 4. 情绪标签审核: emotionEnter
//
// taskId:
// 首次调用 rep/ugc/exam/start 获取,之后调用 rep/ugc/exam/info/get 获取
//
// questionId:
// 调用 rep/ugc/exam/question/single/get 获取
//
// answer:
// A 对, B 错
const createOption = require('../util/option.js')
module.exports = (query, request) => {
if (!query.examType || !query.taskId || !query.questionId || !query.answer)
return Promise.reject({
status: 400,
body: {
code: 400,
message: '参数不足',
},
})
const data = {
examType: query.examType,
taskId: query.taskId,
questionId: query.questionId,
answer: query.answer,
}
return request('/api/rep/ugc/exam/submit', data, createOption(query, 'eapi'))
}
+16
View File
@@ -0,0 +1,16 @@
// 云小编领取一日会员
//
// 注:前提条件见 rep/ugc/user/vip
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
activityId: query.activityId || '5001',
}
return request(
`/api/rep/ugc/user/collect-vip`,
data,
createOption(query, 'eapi'),
)
}
+7
View File
@@ -0,0 +1,7 @@
// 云小编获取用户详情
const createOption = require('../util/option.js')
module.exports = (query, request) => {
return request(`/api/rep/ugc/user/get`, {}, createOption(query, 'eapi'))
}
+7
View File
@@ -0,0 +1,7 @@
// 云小编每日签到
const createOption = require('../util/option.js')
module.exports = (query, request) => {
return request(`/api/rep/ugc/user/sign`, {}, createOption(query, 'eapi'))
}
+12
View File
@@ -0,0 +1,12 @@
// 云小编查询会员任务状态
//
// 状态 (data.status)
// 10: 用户积分达50,可免费领取1日黑胶会员
// 20: 用户积分已达50,可免费领取1日黑胶会员
// 30: 已领取1日黑胶会员,明天再来吧~
const createOption = require('../util/option.js')
module.exports = (query, request) => {
return request(`/api/rep/ugc/user/vip`, {}, createOption(query, 'eapi'))
}
+57 -5
View File
@@ -1,8 +1,42 @@
// 听歌打卡 // 听歌打卡
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
module.exports = (query, request) => { const { APP_CONF } = require('../util/config.json')
const data = { const DOMAIN = APP_CONF.clDomian
module.exports = async (query, request) => {
// 注入 os=osx 的 cookie
let cookie = query.cookie || ''
if (typeof cookie === 'object') {
cookie = Object.assign({ os: 'osx' }, cookie)
} else if (typeof cookie === 'string') {
if (cookie.indexOf('os=') > -1) {
cookie = cookie.replace(/os=[^;]+/g, 'os=osx')
} else {
cookie = cookie + '; os=osx'
}
} else {
cookie = 'os=osx'
}
query.cookie = cookie
// 1) startplay → 进「最近播放」
const startplayData = {
logs: JSON.stringify([
{
action: 'startplay',
json: {
id: query.id,
type: 'song',
mainsite: '1',
mainsiteWeb: '1',
content: `id=${query.sourceid}`,
},
},
]),
}
// 2) play → 涨「听歌排行」计数
const playData = {
logs: JSON.stringify([ logs: JSON.stringify([
{ {
action: 'play', action: 'play',
@@ -15,12 +49,30 @@ module.exports = (query, request) => {
type: 'song', type: 'song',
wifi: 0, wifi: 0,
source: 'list', source: 'list',
mainsite: 1, mainsite: '1',
content: '', mainsiteWeb: '1',
content: `id=${query.sourceid}`,
}, },
}, },
]), ]),
} }
return request(`/api/feedback/weblog`, data, createOption(query, 'weapi')) const option = createOption(query, 'eapi')
option.domain = DOMAIN
// 发送两次请求
const res1 = await request(`/api/feedback/weblog`, startplayData, option)
const res2 = await request(`/api/feedback/weblog`, playData, option)
return {
status: 200,
body: {
code: 200,
data: 'success',
details: {
startplay: res1.body,
play: res2.body,
},
},
}
} }
+128
View File
@@ -0,0 +1,128 @@
// 听歌打卡 - NCBL 加密版 (仿桌面客户端 PLV/PLD 上报)
// 复制自 https://github.com/folltoshe/netease-report-listen-song 的 PC 端日志上报方式
//
// PLV 和 PLD 分两次独立上传
const {
buildPlv,
buildPld,
buildRecords,
extractContext,
parseCookie,
buildCookieStr,
buildMetaJson,
doUpload,
} = require('../util/ncbl')
module.exports = async (query, request) => {
// --- 参数校验 ---
const songId = Number(query.id)
if (!songId || isNaN(songId)) {
return { status: 400, body: { code: 400, msg: '缺少有效的 id (歌曲ID)' } }
}
const playTime = Number(query.time)
if (isNaN(playTime) || playTime <= 0) {
return {
status: 400,
body: { code: 400, msg: '缺少有效的 time (播放时长)' },
}
}
const totalTime = Number(query.total) || playTime
const sourceId = String(query.sourceid || query.sourceId || '')
const sourceName = query.source || 'list'
// --- 解析认证上下文 ---
const rawCookie = query.cookie || ''
const cookieObj = parseCookie(rawCookie)
cookieObj.os = 'pc'
const ctx = extractContext(cookieObj)
// 兜底取 token
if (!ctx.auth.token && rawCookie) {
const parsed =
typeof rawCookie === 'string' ? parseCookie(rawCookie) : rawCookie
ctx.auth.token = parsed.MUSIC_U || ''
}
if (!ctx.auth.token) {
return { status: 401, body: { code: 401, msg: '缺少 MUSIC_U 鉴权令牌' } }
}
// --- 构建歌曲和来源 ---
const song = {
id: songId,
name: query.name || '',
artist: query.artist || '',
bitrate: Number(query.bitrate) || 320,
level: query.level || 'exhigh',
vip: query.vip === 'true' || query.vip === true,
time: totalTime,
}
const source = {
id: sourceId || String(songId),
type: 'track',
name: sourceName,
}
const metaJson = buildMetaJson(ctx)
const cookieStr = buildCookieStr(ctx)
const ts = Math.floor(Date.now() / 1000)
const played = Math.min(playTime, totalTime)
const plvBody = buildRecords([
{ time: ts, action: '_plv', data: buildPlv(ctx, song, source) },
])
const pldBody = buildRecords([
{ time: ts, action: '_pld', data: buildPld(ctx, song, source, played) },
])
try {
// 1) 上传 PLV
const plv = await doUpload(ctx, metaJson, plvBody, cookieStr, 'PLV')
if (!plv.success) {
const rateMsg =
plv.respBody?.data?.rate != null
? ` (rate=${plv.respBody.data.rate})`
: ''
return {
status: 200,
body: {
code: plv.respBody?.code || -1,
msg: `PLV 上报失败${rateMsg}`,
details: plv.respBody,
},
}
}
// 2) 上传 PLD
const pld = await doUpload(ctx, metaJson, pldBody, cookieStr, 'PLD')
if (!pld.success) {
return {
status: 200,
body: {
code: pld.respBody?.code || -1,
msg: 'PLV 成功但 PLD 失败',
details: { plv: plv.respBody, pld: pld.respBody },
},
}
}
return {
status: 200,
body: {
code: 200,
data: 'scrobble_v1 上报成功',
details: {
plv: { fileName: plv.fileName, payloadSize: plv.payload.length },
pld: { fileName: pld.fileName, payloadSize: pld.payload.length },
},
},
}
} catch (err) {
return {
status: 502,
body: { code: 502, msg: `请求异常: ${err.message || err}` },
}
}
}
+5 -1
View File
@@ -7,5 +7,9 @@ module.exports = (query, request) => {
msg: query.msg || '', msg: query.msg || '',
id: query.id || '', id: query.id || '',
} }
return request(`/api/share/friends/resource`, data, createOption(query)) return request(
`/api/share/friends/resource`,
data,
createOption(query, 'xeapi', 'v3'),
)
} }
+9
View File
@@ -0,0 +1,9 @@
// 从云盘获取歌曲下载链接
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
songId: query.id,
}
return request(`/api/cloud/dowonload`, data, createOption(query, 'eapi'))
}
+12 -2
View File
@@ -1,13 +1,23 @@
// 获取客户端歌曲下载链接 - v1 // 获取客户端歌曲下载链接 - v1
// 此版本不再采用 br 作为音质区分的标准 // 此版本不再采用 br 作为音质区分的标准
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断 // 而是采用 standard, exhigh, lossless, hires, jyeffect(高清臻音), vivid(臻音全景声), jymaster(超清母带), sky(沉浸环绕声) 进行音质判断
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
const { cookieToJson } = require('../util/index.js')
module.exports = (query, request) => { module.exports = (query, request) => {
const data = { const data = {
id: query.id, id: query.id,
immerseType: 'c51', immerseType: 'c51',
level: query.level, level: query.level,
} }
return request(`/api/song/enhance/download/url/v1`, data, createOption(query)) const options = createOption(query)
if (query.level === 'vivid') {
const cookie = options.cookie
options.cookie = {
...(typeof cookie === 'string' ? cookieToJson(cookie) : cookie),
os: 'android',
appver: '9.5.61',
}
}
return request(`/api/song/enhance/download/url/v1`, data, options)
} }
+15
View File
@@ -0,0 +1,15 @@
// 插播相似歌曲
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
positionCode: 'toolBarRcmdSong',
resourceId: query.id,
resourceType: 'song',
}
return request(
`/api/link/position/show/resource`,
data,
createOption(query, 'eapi'),
)
}
+15 -7
View File
@@ -1,10 +1,12 @@
// 歌曲链接 - v1 // 歌曲链接 - v1
// 此版本不再采用 br 作为音质区分的标准 // 此版本不再采用 br 作为音质区分的标准
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断 // 而是采用 standard, exhigh, lossless, hires, jyeffect(高清臻音), vivid(臻音全景声), jymaster(超清母带), sky(沉浸环绕声) 进行音质判断
// 当unblock为true时, 会尝试使用unblockmusic-utils进行解锁, 同时音质设置不会生效, 但仍然为必须传入参数 // 当unblock为true时, 会尝试使用unblockmusic-utils进行解锁, 同时音质设置不会生效, 但仍然为必须传入参数
// 当level为sky时, 可通过 immerseType 选择沉浸声类型, 支持 c512(新版c51类型)、ste2(新版环绕立体声类型)、aac2(新版aac类型)、c51(c51类型)、ste(环绕立体声类型)、aac(aac类型), 默认为 c51
const logger = require('../util/logger.js') const logger = require('../util/logger.js')
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
const { cookieToJson } = require('../util/index.js')
module.exports = async (query, request) => { module.exports = async (query, request) => {
const { const {
matchID, matchID,
@@ -15,6 +17,7 @@ module.exports = async (query, request) => {
level: query.level, level: query.level,
encodeType: 'flac', encodeType: 'flac',
} }
const options = createOption(query, 'xeapi')
if (query.unblock === 'true') { if (query.unblock === 'true') {
try { try {
const result = await matchID(query.id, query.source) const result = await matchID(query.id, query.source)
@@ -51,11 +54,16 @@ module.exports = async (query, request) => {
} }
} }
if (data.level == 'sky') { if (data.level == 'sky') {
data.immerseType = 'c51' data.immerseType = query.immerseType || 'c51'
} }
return request( if (data.level == 'vivid') {
`/api/song/enhance/player/url/v1`, data.encodeType = 'mp3'
data, const cookie = options.cookie
createOption(query, 'xeapi'), options.cookie = {
) ...(typeof cookie === 'string' ? cookieToJson(cookie) : cookie),
os: 'android',
appver: '9.5.61',
}
}
return request(`/api/song/enhance/player/url/v1`, data, options)
} }
+17 -4
View File
@@ -1,20 +1,30 @@
// 获取客户端歌曲下载链接 - v1 // 获取客户端歌曲下载链接 - v1
// 此版本不再采用 br 作为音质区分的标准 // 此版本不再采用 br 作为音质区分的标准
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断 // 而是采用 standard, exhigh, lossless, hires, jyeffect(高清臻音), vivid(臻音全景声), jymaster(超清母带), sky(沉浸环绕声) 进行音质判断
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
const { cookieToJson } = require('../util/index.js')
module.exports = async (query, request) => { module.exports = async (query, request) => {
const data = { const data = {
id: query.id, id: query.id,
immerseType: 'c51', immerseType: 'c51',
level: query.level, level: query.level,
} }
const options = createOption(query)
if (query.level === 'vivid') {
const cookie = options.cookie
options.cookie = {
...(typeof cookie === 'string' ? cookieToJson(cookie) : cookie),
os: 'android',
appver: '9.5.61',
}
}
const response = await request( const response = await request(
`/api/song/enhance/download/url/v1`, `/api/song/enhance/download/url/v1`,
data, data,
createOption(query), options,
) )
let url = response?.body?.data?.[0]?.url let url = response?.body?.data?.url || response?.body?.data?.[0]?.url
if (!url) { if (!url) {
const fallbackData = { const fallbackData = {
@@ -25,10 +35,13 @@ module.exports = async (query, request) => {
if (query.level === 'sky') { if (query.level === 'sky') {
fallbackData.immerseType = 'c51' fallbackData.immerseType = 'c51'
} }
if (query.level === 'vivid') {
fallbackData.encodeType = 'mp3'
}
const fallback = await request( const fallback = await request(
`/api/song/enhance/player/url/v1`, `/api/song/enhance/player/url/v1`,
fallbackData, fallbackData,
createOption(query), options,
) )
url = fallback?.body?.data?.[0]?.url url = fallback?.body?.data?.[0]?.url
+22
View File
@@ -0,0 +1,22 @@
// 歌曲百科
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const extJson = {
states: {
playingResource: {
current: query.id,
scene: 'songWiki',
},
},
}
const data = {
extJson: JSON.stringify(extJson),
positionCode: 'songWikiMainPosition',
}
return request(
`/api/link/page/parent/relation/construct/info`,
data,
createOption(query, 'eapi'),
)
}
+20
View File
@@ -0,0 +1,20 @@
// 云小编获取任务
//
// type:
// 1: 歌曲曲风审核 musicalStyleEnter
// 2: 歌曲语种审核 languageEnter
// 3: 歌曲原唱审核 oriSingerEnter
// 4: 情绪标签审核 emotionEnter
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
type: query.type || '4',
}
return request(
`/api/thinktank/audit/resource/detail`,
data,
createOption(query, 'eapi'),
)
}
+36
View File
@@ -0,0 +1,36 @@
// 云小编提交任务
//
// type:
// 1: 歌曲曲风审核 musicalStyleEnter
// 2: 歌曲语种审核 languageEnter
// 3: 歌曲原唱审核 oriSingerEnter
// 4: 情绪标签审核 emotionEnter
//
// taskId:
// 调用 thinktank/audit/resource/detail 获取
//
// judgement:
// 1: 同意, 2: 否决, 3: 跳过 (不算次数)
const createOption = require('../util/option.js')
module.exports = (query, request) => {
if (!query.taskId || !query.judgement)
return Promise.reject({
status: 400,
body: {
code: 400,
message: '参数不足',
},
})
const data = {
type: query.type || '4',
taskId: query.taskId,
judgement: query.judgement,
}
return request(
`/api/thinktank/audit/resource/update`,
data,
createOption(query, 'eapi'),
)
}
+3 -2
View File
@@ -4,9 +4,10 @@ const createOption = require('../util/option.js')
module.exports = (query, request) => { module.exports = (query, request) => {
const data = { const data = {
getcounts: true, getcounts: true,
time: query.lasttime || -1, time: query.lasttime ?? -1,
limit: query.limit || 30, limit: query.limit ?? 30,
total: false, total: false,
fromRN: 'true',
} }
return request(`/api/event/get/${query.uid}`, data, createOption(query)) return request(`/api/event/get/${query.uid}`, data, createOption(query))
} }
+135
View File
@@ -0,0 +1,135 @@
// 获取当前登录用户可被上游枚举的全部动态
const userAccount = require('./user_account.js')
const userEvent = require('./user_event.js')
const PAGE_SIZE = 100
const MAX_PAGES = 1000
const errorResponse = (message, cookie = []) => ({
status: 502,
body: {
code: 502,
message,
},
cookie,
})
module.exports = async (query, request) => {
const accountResult = await userAccount(query, request)
const uid =
accountResult.body?.account?.id || accountResult.body?.profile?.userId
if (!uid) {
if (
accountResult.status !== 200 ||
(accountResult.body?.code && accountResult.body.code !== 200)
) {
return accountResult
}
return {
status: 401,
body: {
code: 401,
message: 'A valid login cookie is required',
},
cookie: accountResult.cookie || [],
}
}
const cookies = [...(accountResult.cookie || [])]
const events = []
const eventIds = new Set()
const cursors = new Set()
let lasttime = -1
let more = true
let pageCount = 0
let size = null
while (more) {
pageCount += 1
const pageResult = await userEvent(
{
...query,
uid,
lasttime,
limit: PAGE_SIZE,
},
request,
)
cookies.push(...(pageResult.cookie || []))
if (pageResult.status !== 200 || pageResult.body?.code !== 200) {
return {
...pageResult,
cookie: cookies,
}
}
if (pageCount === 1) {
const reportedSize = pageResult.body.size
const numericSize = Number(reportedSize)
size =
reportedSize != null && Number.isFinite(numericSize)
? numericSize
: null
}
for (const event of pageResult.body.events || []) {
if (event?.id == null) {
events.push(event)
continue
}
const eventId = String(event.id)
if (!eventIds.has(eventId)) {
eventIds.add(eventId)
events.push(event)
}
}
more = Boolean(pageResult.body.more)
lasttime = pageResult.body.lasttime
if (more) {
const cursor = String(lasttime ?? '')
if (!cursor || cursors.has(cursor)) {
return errorResponse(
'Upstream event pagination cursor stalled',
cookies,
)
}
cursors.add(cursor)
}
if (more && pageCount >= MAX_PAGES) {
return errorResponse(
`Upstream event pagination exceeded ${MAX_PAGES} pages`,
cookies,
)
}
}
const retrievedCount = events.length
const unavailableCount =
size == null ? null : Math.max(size - retrievedCount, 0)
return {
status: 200,
body: {
code: 200,
events,
size,
retrievedCount,
unavailableCount,
sizeMismatch: size == null ? null : size !== retrievedCount,
pageCount,
more: false,
lasttime: lasttime ?? null,
},
cookie: cookies,
}
}
+11
View File
@@ -0,0 +1,11 @@
// 一键领取所有会员成长值
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {}
return request(
`/api/vipnewcenter/app/level/task/reward/getall`,
data,
createOption(query, 'xeapi'),
)
}
+36 -3
View File
@@ -1,7 +1,40 @@
// 黑胶乐签打卡 // 黑胶乐签打卡
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {} module.exports = async (query, request) => {
return request(`/api/vip-center-bff/task/sign`, data, createOption(query)) const results = {}
const taskSign = await request(
'/api/vip-center-bff/task/sign',
{},
createOption(query, 'weapi'),
)
results.taskSign = taskSign.body
const checkinDetail = await request(
'/api/vipnewcenter/app/level/user/checkin/history/detail',
{
signDayTime: Date.now(),
type: 1,
},
createOption(query, 'eapi'),
)
results.checkinDetail = checkinDetail.body
// 两个接口都返回 code=200 即视为打卡成功
const signed =
Number(results.taskSign?.code) === 200 &&
Number(results.checkinDetail?.code) === 200
return {
body: {
code: 200,
...results,
signed,
message: signed ? '黑胶乐签打卡成功' : '黑胶乐签打卡失败',
},
cookie: taskSign.cookie,
status: 200,
}
} }
+14
View File
@@ -0,0 +1,14 @@
// 黑胶乐签打卡详情
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
signDayTime: query.timestamp,
type: '1',
}
return request(
`/api/vipnewcenter/app/level/user/checkin/history/detail`,
data,
createOption(query, 'eapi'),
)
}
+14
View File
@@ -0,0 +1,14 @@
// 黑胶乐签打卡历史 / 状态查询
// 支持传入 type=0(用户信息栏)或 type=1(黑胶乐签)
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
type: query.type || '0',
}
return request(
`/api/vipnewcenter/app/minidesk/music/sign/pc`,
data,
createOption(query, 'eapi'),
)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// 黑胶乐签签到信息 // 黑胶乐签未来签到信息
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
module.exports = (query, request) => { module.exports = (query, request) => {
+14
View File
@@ -0,0 +1,14 @@
// 会员任务 - 新版
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
taskType: 'app_vip_task_center',
userId: query.id,
}
return request(
`/api/middle/vip/mission/user/progress/list`,
data,
createOption(query, 'xeapi'),
)
}
+38 -50
View File
@@ -2,6 +2,7 @@ const { default: axios } = require('axios')
const fs = require('fs') const fs = require('fs')
var xml2js = require('xml2js') var xml2js = require('xml2js')
const uploadPlugin = require('../plugins/upload')
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
const { getFileExtension, readFileChunk } = require('../util/fileHelper') const { getFileExtension, readFileChunk } = require('../util/fileHelper')
@@ -19,15 +20,7 @@ function createDupkey() {
return s.join('') return s.join('')
} }
module.exports = async (query, request) => { module.exports = async (query, request, dependencies = {}) => {
const ext = getFileExtension(query.songFile.name)
const filename =
query.songName ||
query.songFile.name
.replace('.' + ext, '')
.replace(/\s/g, '')
.replace(/\./g, '_')
if (!query.songFile) { if (!query.songFile) {
return Promise.reject({ return Promise.reject({
status: 500, status: 500,
@@ -38,6 +31,19 @@ module.exports = async (query, request) => {
}) })
} }
const axiosRequest = dependencies.axios || axios
const uploadImage = dependencies.uploadPlugin || uploadPlugin
const ext = getFileExtension(query.songFile.name)
const filename =
query.songName ||
query.songFile.name
.replace('.' + ext, '')
.replace(/\s/g, '')
.replace(/\./g, '_')
const coverImgId = query.imgFile
? (await uploadImage(query, request)).imgId
: query.coverImgId
const tokenRes = await request( const tokenRes = await request(
`/api/nos/token/alloc`, `/api/nos/token/alloc`,
{ {
@@ -53,7 +59,7 @@ module.exports = async (query, request) => {
const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F') const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F')
const docId = tokenRes.body.result.docId const docId = tokenRes.body.result.docId
const res = await axios({ const res = await axiosRequest({
method: 'post', method: 'post',
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploads`, url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploads`,
headers: { headers: {
@@ -94,7 +100,7 @@ module.exports = async (query, request) => {
) )
} }
const res3 = await axios({ const res3 = await axiosRequest({
method: 'put', method: 'put',
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?partNumber=${blockIndex}&uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`, url: `https://ymusic.nos-hz.163yun.com/${objectKey}?partNumber=${blockIndex}&uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
headers: { headers: {
@@ -117,7 +123,7 @@ module.exports = async (query, request) => {
} }
completeStr += '</CompleteMultipartUpload>' completeStr += '</CompleteMultipartUpload>'
await axios({ await axiosRequest({
method: 'post', method: 'post',
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`, url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
headers: { headers: {
@@ -128,29 +134,29 @@ module.exports = async (query, request) => {
data: completeStr, data: completeStr,
}) })
const voiceData = JSON.stringify([
{
name: filename,
autoPublish: query.autoPublish == 1 ? true : false,
autoPublishText: query.autoPublishText || '',
description: query.description,
voiceListId: query.voiceListId,
coverImgId,
dfsId: docId,
categoryId: query.categoryId,
secondCategoryId: query.secondCategoryId,
composedSongs: query.composedSongs ? query.composedSongs.split(',') : [],
privacy: query.privacy == 1 ? true : false,
publishTime: query.publishTime || 0,
orderNo: query.orderNo || 1,
},
])
await request( await request(
`/api/voice/workbench/voice/batch/upload/preCheck`, `/api/voice/workbench/voice/batch/upload/preCheck`,
{ {
dupkey: createDupkey(), dupkey: createDupkey(),
voiceData: JSON.stringify([ voiceData,
{
name: filename,
autoPublish: query.autoPublish == 1 ? true : false,
autoPublishText: query.autoPublishText || '',
description: query.description,
voiceListId: query.voiceListId,
coverImgId: query.coverImgId,
dfsId: docId,
categoryId: query.categoryId,
secondCategoryId: query.secondCategoryId,
composedSongs: query.composedSongs
? query.composedSongs.split(',')
: [],
privacy: query.privacy == 1 ? true : false,
publishTime: query.publishTime || 0,
orderNo: query.orderNo || 1,
},
]),
}, },
{ {
...createOption(query), ...createOption(query),
@@ -163,25 +169,7 @@ module.exports = async (query, request) => {
`/api/voice/workbench/voice/batch/upload/v2`, `/api/voice/workbench/voice/batch/upload/v2`,
{ {
dupkey: createDupkey(), dupkey: createDupkey(),
voiceData: JSON.stringify([ voiceData,
{
name: filename,
autoPublish: query.autoPublish == 1 ? true : false,
autoPublishText: query.autoPublishText || '',
description: query.description,
voiceListId: query.voiceListId,
coverImgId: query.coverImgId,
dfsId: docId,
categoryId: query.categoryId,
secondCategoryId: query.secondCategoryId,
composedSongs: query.composedSongs
? query.composedSongs.split(',')
: [],
privacy: query.privacy == 1 ? true : false,
publishTime: query.publishTime || 0,
orderNo: query.orderNo || 1,
},
]),
}, },
{ {
...createOption(query), ...createOption(query),
+5 -1
View File
@@ -1,5 +1,9 @@
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
module.exports = (query, request) => { module.exports = (query, request) => {
const data = {} const data = {}
return request(`/api/pointmall/user/sign`, data, createOption(query, 'weapi')) return request(
`/api/pointmall/user/sign`,
data,
createOption(query, 'xeapi', 'v3'),
)
} }
+26
View File
@@ -0,0 +1,26 @@
// 云贝广告任务 - 完成任务领取云贝
// 逆向来源: 云贝任务中心 H5 (st.music.163.com/yunbei-listen) main.js
// POST /api/ad/power/yunbei/distribution/create
// 参数: yunbeiAmount (单次可得云贝, 客户端从 list 接口的 singleAmount 取值, 当前为 150)
// 返回: true (领取成功)
//
// 实测验证 (2026-08-05):
// - 仅传 yunbeiAmount 即可成功领取, 无需真实听歌/看视频
// - 单日上限 10 次 × 150 云贝 = 1500 云贝/天
// - 超限返回 code:400 "单日完成任务数已达上限"
// - 无频率限制, 800ms 间隔连续调用均成功
//
// 建议: 领取前先调 yunbei_ad_task_list 查询 times, 达到 10 次即停止
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
yunbeiAmount: query.yunbeiAmount || 150,
}
return request(
`/api/ad/power/yunbei/distribution/create`,
data,
createOption(query, 'weapi'),
)
}
+16
View File
@@ -0,0 +1,16 @@
// 云贝广告任务 - 查询今日任务状态
// 逆向来源: 云贝任务中心 H5 (st.music.163.com/yunbei-listen) main.js
// GET /api/ad/power/yunbei/distribution/list
// 返回: { times: 今日已完成次数, amount: 今日累计云贝, singleAmount: 单次可得云贝 }
// 注意: 单日上限 10 次, 单次 150 云贝 (每天最多 1500)
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {}
return request(
`/api/ad/power/yunbei/distribution/list`,
data,
createOption(query, 'weapi'),
)
}
+20
View File
@@ -0,0 +1,20 @@
// 云贝广告任务 - 获取推荐歌曲
// 逆向来源: 云贝任务中心 H5 (st.music.163.com/yunbei-listen) main.js
// POST /api/ad/power/yunbei/distribution/recommend/song
// 参数: offset (默认 0), limit (默认 10, 客户端一次 10 首)
// 返回: 推荐歌曲数组 [{ songId, songName, artistName, albumUrl, songChorusStartTime, likeFlag, alg }]
// 注意: alg 均为 alg_payrec_yunBei_*, 为"听歌得云贝"任务专属推荐
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
offset: query.offset || 0,
limit: query.limit || 10,
}
return request(
`/api/ad/power/yunbei/distribution/recommend/song`,
data,
createOption(query, 'weapi'),
)
}
+4 -3
View File
@@ -1,10 +1,11 @@
import { banner, lyric } from '@neteasecloudmusicapienhanced/api' import { banner, lyric } from '@neteasecloudmusicapienhanced/api'
import { logger } from '../util/logger.js' import type { Response } from '@neteasecloudmusicapienhanced/api'
banner({ type: 0 }).then((res) => { import logger from '../util/logger.js'
banner({ type: 0 }).then((res: Response) => {
logger.info(res) logger.info(res)
}) })
lyric({ lyric({
id: '33894312', id: '33894312',
}).then((res) => { }).then((res: Response) => {
logger.info(res) logger.info(res)
}) })
+48
View File
@@ -0,0 +1,48 @@
const {
login_cellphone,
yunbei_ad_task_list,
yunbei_ad_task_recommend_song,
yunbei_ad_task_finish,
} = require('../main')
// 云贝广告任务(听歌/看视频得云贝)完整流程示例
// 1. list - 查询今日任务状态(次数/云贝)
// 2. finish - 领取云贝, 仅需传 yunbeiAmount(单次 150), 无需真实听歌/看视频
// 3. recommend/song - 可选, 获取推荐歌曲列表
// 注意: 单日上限 10 次 x 150 = 1500 云贝/天
async function main() {
const login = await login_cellphone({
phone: '手机号',
password: '密码',
})
const cookie = login.body.cookie
// 1. 查询今日任务状态
const list = await yunbei_ad_task_list({ cookie })
const { times, amount, singleAmount } = list.body
console.log(
`今日已完成 ${times} 次, 累计 ${amount} 云贝, 单次可得 ${singleAmount} 云贝`,
)
if (times >= 10) {
console.log('已达单日上限, 明天再来吧')
return
}
// 2. 领取云贝(单次 150)
const finish = await yunbei_ad_task_finish({
yunbeiAmount: singleAmount,
cookie,
})
console.log('领取结果:', finish.body)
// 3. 获取推荐歌曲(听歌任务专属推荐, 可选)
const rcmd = await yunbei_ad_task_recommend_song({
offset: 0,
limit: 10,
cookie,
})
console.log('推荐歌曲:', rcmd.body.map((s) => s.songName).join(', '))
}
main()
+35 -19
View File
@@ -1,7 +1,7 @@
{ {
"name": "@neteasecloudmusicapienhanced/api", "name": "@neteasecloudmusicapienhanced/api",
"version": "4.34.0", "version": "4.41.1",
"description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护", "description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced)",
"scripts": { "scripts": {
"dev": "nodemon app.js", "dev": "nodemon app.js",
"start": "node app.js", "start": "node app.js",
@@ -9,9 +9,11 @@
"lint": "eslint \"**/*.{js,ts}\"", "lint": "eslint \"**/*.{js,ts}\"",
"lint-fix": "eslint --fix \"**/*.{js,ts}\"", "lint-fix": "eslint --fix \"**/*.{js,ts}\"",
"prepare": "husky install", "prepare": "husky install",
"pkgwin": "pkg . -t node18-win-x64 -C GZip -o precompiled/app", "docs:format": "node scripts/format-docs.js",
"pkglinux": "pkg . -t node18-linux-x64 -C GZip -o precompiled/app", "docs:check": "node scripts/format-docs.js --check",
"pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o precompiled/app" "pkgwin": "node scripts/build-pkg.js . -t node18-win-x64 -C GZip -o precompiled/app",
"pkglinux": "node scripts/build-pkg.js . -t node18-linux-x64 -C GZip -o precompiled/app",
"pkgmacos": "node scripts/build-pkg.js . -t node18-macos-x64 -C GZip -o precompiled/app"
}, },
"bin": "./app.js", "bin": "./app.js",
"pkg": { "pkg": {
@@ -41,6 +43,14 @@
], ],
"main": "main.js", "main": "main.js",
"types": "./interface.d.ts", "types": "./interface.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git"
},
"bugs": {
"url": "https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced/issues"
},
"homepage": "https://neteasecloudmusicapienhanced.js.org/",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -65,44 +75,50 @@
"data" "data"
], ],
"dependencies": { "dependencies": {
"@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.3.2", "@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.4.5",
"axios": "^1.16.1", "axios": "^1.20.0",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"express": "^5.2.1", "express": "^5.2.1",
"express-fileupload": "^1.5.2", "express-fileupload": "^1.5.2",
"fzstd": "^0.1.1",
"gzip": "^0.1.0", "gzip": "^0.1.0",
"music-metadata": "^11.12.3", "jsdom": "^24.1.3",
"music-metadata": "^11.16.1",
"node-forge": "^1.4.0", "node-forge": "^1.4.0",
"pac-proxy-agent": "^7.2.0", "pac-proxy-agent": "^7.2.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"safe-decode-uri-component": "^1.2.1", "safe-decode-uri-component": "^1.2.1",
"tunnel": "^0.0.6", "tunnel": "^0.0.6",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
"yargs": "^18.0.0" "yargs": "^18.2.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.5", "@eslint/eslintrc": "^3.3.7",
"@eslint/js": "^9.39.4", "@eslint/js": "^9.39.5",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/express-fileupload": "^1.5.1", "@types/express-fileupload": "^1.5.1",
"@types/mocha": "^10.0.10", "@types/mocha": "^10.0.10",
"@types/node": "25.5.0", "@types/node": "25.9.2",
"@typescript-eslint/eslint-plugin": "^8.60.0", "@typescript-eslint/eslint-plugin": "^8.71.0",
"@typescript-eslint/parser": "^8.60.0", "@typescript-eslint/parser": "^8.71.0",
"eslint": "^9.39.4", "eslint": "^9.39.5",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-html": "^8.1.4", "eslint-plugin-html": "^8.2.1",
"eslint-plugin-prettier": "^5.5.6", "eslint-plugin-prettier": "^5.5.6",
"globals": "^17.6.0", "globals": "^17.13.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"intelli-espower-loader": "^1.1.0", "intelli-espower-loader": "^1.1.0",
"lint-staged": "^16.4.0", "lint-staged": "^16.4.0",
"mocha": "^11.7.6", "mocha": "^11.8.0",
"nodemon": "^3.1.14", "nodemon": "^3.1.14",
"pkg": "^5.8.1", "pkg": "^5.8.1",
"power-assert": "^1.6.1", "power-assert": "^1.6.1",
"prettier": "^3.8.3", "prettier": "^3.9.9",
"typescript": "^5.9.3" "typescript": "^5.9.3"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
} }
} }
+722 -374
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
packages:
- '.'
allowBuilds:
core-js: true
es5-ext: true
minimumReleaseAgeExclude:
- '@neteasecloudmusicapienhanced/[email protected]'
+495 -44
View File
@@ -115,6 +115,182 @@
textarea:focus { textarea:focus {
border-color: #333; border-color: #333;
} }
/* ===== 自定义选项区域 ===== */
.section-container {
margin-top: 20px;
padding: 16px 20px;
background: #fafafa;
border: 1px solid #e8e8e8;
border-radius: 8px;
}
.section-container + .section-container {
margin-top: 12px;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: #555;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.section-title .count-badge {
font-size: 11px;
background: #ddd;
color: #666;
padding: 1px 8px;
border-radius: 10px;
font-weight: 400;
}
.dynamic-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
align-items: center;
}
.dynamic-row .key-input {
flex: 0 0 200px;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 13px;
outline: none;
font-family: 'Courier New', monospace;
}
.dynamic-row .value-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 13px;
outline: none;
}
.dynamic-row .key-input:focus,
.dynamic-row .value-input:focus {
border-color: #333;
}
.remove-btn {
width: 28px;
height: 28px;
padding: 0;
background: #fff;
color: #999;
border: 1px solid #ddd;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.remove-btn:hover {
background: #fee;
color: #e33;
border-color: #fcc;
}
.add-btn {
background: #fff;
color: #333;
border: 1px dashed #ccc;
padding: 6px 16px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
margin-top: 4px;
}
.add-btn:hover {
border-color: #999;
background: #f5f5f5;
}
/* ===== 参数编辑区 Tab ===== */
.param-container {
flex: 1;
display: flex;
flex-direction: column;
}
.param-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.param-header-title {
font-size: 14px;
font-weight: 500;
color: #555;
}
.param-tabs {
display: flex;
gap: 0;
background: #f0f0f0;
border-radius: 6px;
padding: 2px;
}
.tab-btn {
padding: 4px 14px;
border: none;
background: transparent;
color: #888;
font-size: 12px;
font-weight: 500;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
}
.tab-btn:hover {
color: #555;
}
.tab-btn.active {
background: #fff;
color: #333;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.param-panel {
flex: 1;
display: flex;
flex-direction: column;
}
.param-panel textarea {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 13px;
resize: vertical;
min-height: 350px;
outline: none;
}
.param-panel textarea:focus {
border-color: #333;
}
</style> </style>
</head> </head>
<body> <body>
@@ -125,6 +301,16 @@
<label for="uri">URI</label> <label for="uri">URI</label>
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1"> <input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
</div> </div>
<div class="form-row">
<label for="method">请求方法</label>
<select id="method" name="method">
<option value="POST" selected>POST</option>
<option value="GET">GET</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="PATCH">PATCH</option>
</select>
</div>
<div class="form-row"> <div class="form-row">
<label for="crypto">加密方式</label> <label for="crypto">加密方式</label>
<select id="crypto" name="crypto"> <select id="crypto" name="crypto">
@@ -133,22 +319,45 @@
<option value="api">api</option> <option value="api">api</option>
<option value="linuxapi">linuxapi</option> <option value="linuxapi">linuxapi</option>
<option value="xeapi">xeapi</option> <option value="xeapi">xeapi</option>
<option value="neapi">neapi</option>
<option value="" selected>(默认)</option> <option value="" selected>(默认)</option>
</select> </select>
</div> </div>
<div class="form-row">
<label for="timeout">超时(ms)</label>
<input type="number" id="timeout" name="timeout" value="30000" min="0" step="1000" placeholder="0 表示无超时">
</div>
<div class="form-row"> <div class="form-row">
<label></label> <label></label>
<button type="submit">发送请求</button> <button type="submit">发送请求</button>
</div> </div>
</form> </form>
<!-- ===== 自定义请求头 ===== -->
<div class="section-container" id="headersSection">
<div class="section-title">
请求头 (Headers)
<span class="count-badge" id="headerCount">4</span>
</div>
<div id="headersList"></div>
<button class="add-btn" onclick="addHeaderRow('', '')">+ 添加请求头</button>
</div>
<div class="data-result"> <div class="data-result">
<div> <div>
<label for="result">响应结果</label> <label for="result">响应结果</label>
<textarea id="result" name="result" readonly></textarea> <textarea id="result" name="result" readonly></textarea>
</div> </div>
<div> <div class="param-container">
<label for="data">请求数据</label> <div class="param-header">
<textarea id="data" name="data"> <span class="param-header-title">请求参数</span>
<div class="param-tabs">
<button class="tab-btn active" data-mode="json" onclick="switchParamTab('json')">JSON</button>
<button class="tab-btn" data-mode="kv" onclick="switchParamTab('kv')">键值对</button>
</div>
</div>
<div class="param-panel" id="jsonPanel">
<textarea id="data" name="data">
{ {
"cp": false, "cp": false,
"id": "2058263032", "id": "2058263032",
@@ -160,64 +369,306 @@
"ytv": 0, "ytv": 0,
"yv": 0 "yv": 0
}</textarea> }</textarea>
</div>
<div class="param-panel" id="kvPanel" style="display:none">
<div id="kvList"></div>
<button class="add-btn" onclick="addKvRow('', '')">+ 添加参数</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script> <script>
async function sendRequest() { // ===== 动态行管理 =====
document.getElementById('result').value = "" function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
const uri = document.getElementById('uri').value; function addHeaderRow(key, value) {
const crypto = document.getElementById('crypto').value; const list = document.getElementById('headersList');
const data = document.getElementById('data').value; const row = document.createElement('div');
row.className = 'dynamic-row';
row.innerHTML =
'<input type="text" class="key-input" placeholder="Header 名称" value="' +
escapeHtml(key) +
'">' +
'<input type="text" class="value-input" placeholder="值" value="' +
escapeHtml(value) +
'">' +
'<button class="remove-btn" onclick="removeRow(this)" title="删除">✕</button>';
list.appendChild(row);
updateCount();
}
function addKvRow(key, value) {
const list = document.getElementById('kvList');
const row = document.createElement('div');
row.className = 'dynamic-row';
row.innerHTML =
'<input type="text" class="key-input" placeholder="参数名" value="' +
escapeHtml(key) +
'">' +
'<input type="text" class="value-input" placeholder="值" value="' +
escapeHtml(value) +
'">' +
'<button class="remove-btn" onclick="removeRow(this)" title="删除">✕</button>';
list.appendChild(row);
}
function removeRow(btn) {
btn.parentElement.remove();
}
function collectHeaders() {
const headers = {};
document.querySelectorAll('#headersList .dynamic-row').forEach(function (row) {
var key = row.querySelector('.key-input').value.trim();
var value = row.querySelector('.value-input').value;
if (key) headers[key] = value;
});
return headers;
}
function collectKvData() {
var obj = {};
document.querySelectorAll('#kvList .dynamic-row').forEach(function (row) {
var key = row.querySelector('.key-input').value.trim();
var val = row.querySelector('.value-input').value;
if (!key) return;
// 自动类型转换
if (val === 'true') obj[key] = true;
else if (val === 'false') obj[key] = false;
else if (val === 'null') obj[key] = null;
else if (/^-?\d+(\.\d+)?$/.test(val) && !isNaN(Number(val))) obj[key] = Number(val);
else obj[key] = val;
});
return obj;
}
// ===== Tab 切换 =====
function syncJsonToKv() {
try { try {
const res = await axios({ var data = JSON.parse(document.getElementById('data').value);
url: `/api?timestamp=${Date.now()}`, var list = document.getElementById('kvList');
method: 'post', list.innerHTML = '';
data: { Object.keys(data).forEach(function (k) {
uri: uri, addKvRow(k, String(data[k]));
data: JSON.parse(data),
crypto: crypto,
},
}); });
return true;
document.getElementById('result').value = JSON.stringify(res.data, null, 2); } catch (e) {
} catch (error) { return false;
document.getElementById('result').value = 'Request failed: ' + error.message;
} }
} }
(function fillFromQuery() { function syncKvToJson() {
const params = new URLSearchParams(window.location.search); var obj = collectKvData();
if (!params.toString()) return; document.getElementById('data').value = JSON.stringify(obj, null, 2);
const uri = params.get('uri'); }
const crypto = params.get('crypto');
const dataParam = params.get('data'); function switchParamTab(mode) {
if (uri) { var jsonPanel = document.getElementById('jsonPanel');
document.getElementById('uri').value = uri; var kvPanel = document.getElementById('kvPanel');
if (mode === 'kv') {
if (!syncJsonToKv()) {
alert('JSON 格式有误,无法切换到键值对模式,请先修正 JSON');
return;
}
jsonPanel.style.display = 'none';
kvPanel.style.display = 'block';
} else {
syncKvToJson();
kvPanel.style.display = 'none';
jsonPanel.style.display = 'block';
} }
if (crypto) {
const cryptoSelect = document.getElementById('crypto'); document.querySelectorAll('.tab-btn').forEach(function (btn) {
if ([...cryptoSelect.options].some((opt) => opt.value === crypto)) { btn.classList.toggle('active', btn.dataset.mode === mode);
cryptoSelect.value = crypto; });
} }
// ===== 默认请求头(直接套用后端预设值)=====
var defaultHeaders = [
{ key: 'Cookie', value: '' },
{
key: 'User-Agent',
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0',
},
{ key: 'Referer', value: 'https://music.163.com' },
{ key: 'Content-Type', value: 'application/json' },
];
// ===== 发送请求 =====
async function sendRequest() {
document.getElementById('result').value = '';
var uri = document.getElementById('uri').value;
var method = document.getElementById('method').value;
var crypto = document.getElementById('crypto').value;
var timeout = parseInt(document.getElementById('timeout').value, 10) || 0;
var headers = collectHeaders();
// 根据当前激活的 tab 读取数据
var parsedData;
var activeTab = document.querySelector('.tab-btn.active');
if (activeTab && activeTab.dataset.mode === 'kv') {
parsedData = collectKvData();
} else {
var rawData = document.getElementById('data').value;
try {
parsedData = rawData.trim() ? JSON.parse(rawData) : {};
} catch (e) {
document.getElementById('result').value =
'请求数据格式错误:' + e.message;
return;
}
} }
if (dataParam) {
const decoded = dataParam; var axiosConfig = {
try { url: '/api?timestamp=' + Date.now(),
document.getElementById('data').value = JSON.stringify( method: method,
JSON.parse(decoded), };
null,
2, if (timeout > 0) {
axiosConfig.timeout = timeout;
}
if (method === 'GET') {
axiosConfig.params = {
uri: uri,
crypto: crypto,
data: JSON.stringify(parsedData),
headers: Object.keys(headers).length ? JSON.stringify(headers) : undefined,
};
} else {
axiosConfig.data = {
uri: uri,
crypto: crypto,
data: parsedData,
headers: Object.keys(headers).length ? headers : undefined,
};
}
try {
var res = await axios(axiosConfig);
document.getElementById('result').value = JSON.stringify(
res.data,
null,
2,
); );
} catch (error) { } catch (error) {
document.getElementById('data').value = decoded; document.getElementById('result').value =
} 'Request failed: ' + error.message;
} }
})(); }
// ===== 从 URL 查询参数恢复 =====
(function fillFromQuery() {
var params = new URLSearchParams(window.location.search);
if (!params.toString()) {
// 没有查询参数,用默认 header
defaultHeaders.forEach(function (h) {
addHeaderRow(h.key, h.value);
});
return;
}
var uri = params.get('uri');
var crypto = params.get('crypto');
var dataParam = params.get('data');
var method = params.get('method');
var timeout = params.get('timeout');
var headersParam = params.get('headers');
var kvParams = params.get('kvParams');
var paramMode = params.get('paramMode');
if (uri) document.getElementById('uri').value = uri;
if (method) {
var methodSelect = document.getElementById('method');
if (
[].slice.call(methodSelect.options).some(function (opt) {
return opt.value === method;
})
) {
methodSelect.value = method;
}
}
if (timeout) document.getElementById('timeout').value = timeout;
if (crypto) {
var cryptoSelect = document.getElementById('crypto');
if (
[].slice.call(cryptoSelect.options).some(function (opt) {
return opt.value === crypto;
})
) {
cryptoSelect.value = crypto;
}
}
if (dataParam) {
try {
document.getElementById('data').value = JSON.stringify(
JSON.parse(dataParam),
null,
2,
);
} catch (_) {
document.getElementById('data').value = dataParam;
}
}
if (headersParam) {
try {
var parsed = JSON.parse(headersParam);
Object.keys(parsed).forEach(function (k) {
addHeaderRow(k, parsed[k]);
});
} catch (_) {
// fallback: 用默认 header
defaultHeaders.forEach(function (h) {
addHeaderRow(h.key, h.value);
});
}
} else {
defaultHeaders.forEach(function (h) {
addHeaderRow(h.key, h.value);
});
}
if (kvParams) {
try {
var parsed = JSON.parse(kvParams);
// 先填充 kv 列表,再切换到 kv 模式
Object.keys(parsed).forEach(function (k) {
addKvRow(k, String(parsed[k]));
});
if (paramMode === 'kv') {
switchParamTab('kv');
}
} catch (_) {
/* ignore */
}
} else if (paramMode === 'kv' && dataParam) {
// 从 data 参数同步到键值对模式
try {
var data = JSON.parse(dataParam);
Object.keys(data).forEach(function (k) {
addKvRow(k, String(data[k]));
});
switchParamTab('kv');
} catch (_) {
/* ignore */
}
}
})();
</script> </script>
</body> </body>
</html> </html>
+7 -3
View File
@@ -255,9 +255,13 @@
} }
logs.write(`[index] 查询完成。结果数量=${resp.data.result.length}`) logs.write(`[index] 查询完成。结果数量=${resp.data.result.length}`)
for (var song of resp.data.result) { for (var song of resp.data.result) {
logs.write( var a = document.createElement('a');
`[result] <a target="_blank" href="https://music.163.com/song?id=${song.song.id}">${song.song.name} - ${song.song.album.name} (${song.startTime / 1000}s)</a>` a.href = 'https://music.163.com/song?id=' + encodeURIComponent(song.song.id);
) a.target = '_blank';
a.textContent = song.song.name + ' - ' + song.song.album.name + ' (' + (song.startTime / 1000) + 's)';
logs.appendChild(document.createTextNode('[result] '));
logs.appendChild(a);
logs.appendChild(document.createElement('br'));
} }
}) })
}) })
+7 -6
View File
@@ -1,13 +1,14 @@
# 网易云音乐 API Enhanced # NeteaseCloudMusicAPI Enhanced
> 🔍 网易云音乐API Node.js服务的复兴项目 > 🎉 全网收集最全的网易云音乐api接口 基于[NeteaseCloudMusicAPI](https://github.com/binaryify/NeteaseCloudMusicApi)的复刻版本
- 基于原版网易云API新增更多有趣的功能 - ⚡ 四种加密模式 · 后端代理
- 具备登录接口,多达200多个接口 - 🪛 具备多达200多个接口
- 更完善的文档 - 📄 更完善的文档
[Github](https://github.com/neteasecloudmusicapienhanced/api-enhanced) [Github](https://github.com/neteasecloudmusicapienhanced/api-enhanced)
[Get Started](#neteasecloudmusicapienhanced) [前往本家](https://github.com/binaryify/NeteaseCloudMusicApi)
[快速开始](#neteasecloudmusicapienhanced)
![color](#ffffff) ![color](#ffffff)
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

+651 -23
View File
@@ -2,8 +2,6 @@
网易云音乐 NodeJS API Enhanced 网易云音乐 NodeJS API Enhanced
最后更新于: 2026.2.15
## 灵感来自 ## 灵感来自
[disoul/electron-cloud-music](https://github.com/disoul/electron-cloud-music) [disoul/electron-cloud-music](https://github.com/disoul/electron-cloud-music)
@@ -90,6 +88,7 @@ v4.29.9 加入了生成随机中国 IP 功能, 在请求时加上 `randomCNIP=tr
5. 直接点`Continue` 5. 直接点`Continue`
6. `PROJECT NAME`自己填,`FRAMEWORK PRESET` 选 `Other` 然后直接点 `Deploy` 接着等部署完成即可 6. `PROJECT NAME`自己填,`FRAMEWORK PRESET` 选 `Other` 然后直接点 `Deploy` 接着等部署完成即可
## 腾讯云 serverless 部署 ## 腾讯云 serverless 部署
因 `Vercel` 在国内访问太慢(不绑定自己的域名的情况下),在此提供腾讯云 serverless 部署方法 因 `Vercel` 在国内访问太慢(不绑定自己的域名的情况下),在此提供腾讯云 serverless 部署方法
@@ -103,6 +102,7 @@ v4.29.9 加入了生成随机中国 IP 功能, 在请求时加上 `randomCNIP=tr
5. 输入`应用名`,上传方式选择`代码仓库`,进行 GitHub 授权(如已授权可跳过这一步),代码仓库选择刚刚 fork 的项目 5. 输入`应用名`,上传方式选择`代码仓库`,进行 GitHub 授权(如已授权可跳过这一步),代码仓库选择刚刚 fork 的项目
6. 启动文件填入: 6. 启动文件填入:
``` ```
#!/bin/bash #!/bin/bash
export PORT=9000 export PORT=9000
@@ -115,6 +115,7 @@ export PORT=9000
- 腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费 - 腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费
- 当前(2024-08-24), 用此法创建的话, 会`默认`关联一个"日志服务-日志主题"(创建过程中没有提醒), 此服务是计量收费的 - 当前(2024-08-24), 用此法创建的话, 会`默认`关联一个"日志服务-日志主题"(创建过程中没有提醒), 此服务是计量收费的
## 可以使用代理 ## 可以使用代理
在 query 参数中加上 proxy=your-proxy 即可让这一次的请求使用 proxy 在 query 参数中加上 proxy=your-proxy 即可让这一次的请求使用 proxy
@@ -182,6 +183,7 @@ request 相关的环境变量
5. no_proxy 5. no_proxy
6. NO_PROXY 6. NO_PROXY
```shell ```shell
docker pull moefurina/ncm-api docker pull moefurina/ncm-api
@@ -214,12 +216,17 @@ $ sudo docker run -d -p 3000:3000 netease-music-api
- 大部分请求参数或返回内容可在 `/api_decrypt.html` 里解析 - 大部分请求参数或返回内容可在 `/api_decrypt.html` 里解析
- 请求参数模式下, 解密结果可直接带到 `/api.html` 继续调试 - 请求参数模式下, 解密结果可直接带到 `/api.html` 继续调试
- 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持 - 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持
- 目前支持算法 有 `weapi`, `eapi`, `linuxapi` 和 `xeapi` (xeapi 是一种不加密的特殊算法, 主要用于调试加密前的原始请求参数) - 目前支持算法 有 `weapi`, `eapi`, `linuxapi`, `xeapi` 和 `neapi` (xeapi 是一种不加密的特殊算法, 主要用于调试加密前的原始请求参数; neapi 走独立的压缩加密通道, 配置由 `/register/neapikey` 自动获取与刷新)
## 接口文档 ## 接口文档
### 调用前须知 ### 调用前须知
AI 生成的图,仅供娱乐()
![ai generated](./aigen.png)
!> 本项目不提供线上 demo, 只提供在线文档服务, 请不要轻易信任使用他人提供的公开服务,以免发生安全问题,泄露自己的账号和密码 !> 本项目不提供线上 demo, 只提供在线文档服务, 请不要轻易信任使用他人提供的公开服务,以免发生安全问题,泄露自己的账号和密码
!> 为使用方便,降低门槛, 文档示例接口直接使用了 GET 请求,本项目同时支持 GET/POST 请按实际需求使用 (POST 请求 url 必须添加时间戳,使每次请求 url 不一样,不然请求会被缓存) !> 为使用方便,降低门槛, 文档示例接口直接使用了 GET 请求,本项目同时支持 GET/POST 请按实际需求使用 (POST 请求 url 必须添加时间戳,使每次请求 url 不一样,不然请求会被缓存)
@@ -276,7 +283,9 @@ $ sudo docker run -d -p 3000:3000 netease-music-api
`md5_password`: md5 加密后的密码,传入后 `password` 参数将失效 `md5_password`: md5 加密后的密码,传入后 `password` 参数将失效
`captcha`: 验证码,使用 [`/captcha/sent`](#发送验证码)接口传入手机号获取验证码,调用此接口传入验证码,可使用验证码登录,传入后 `password` 参数将失效 `captcha`: 验证码,使用 `/captcha/sent` 或 `/captcha/sent/v1`接口传入手机号获取验证码,调用此接口传入验证码,可使用验证码登录,传入后 `password` 参数将失效
`sca`: 网易易盾滑块验证token, 获取方式未知
**接口地址 :** `/login/cellphone` **接口地址 :** `/login/cellphone`
@@ -390,6 +399,19 @@ body {
**调用例子 :** `/captcha/sent?phone=13xxx` **调用例子 :** `/captcha/sent?phone=13xxx`
### 新版发送验证码
说明 : 调用此接口 ,传入手机号码, 可发送验证码
**必选参数 :** `phone`: 手机号码
**可选参数 :**
`ctcode`: 国家区号,默认 86 即中国
**接口地址 :** `/captcha/sent/v1`
**调用例子 :** `/captcha/sent/v1?phone=13xxx`
### 验证验证码 ### 验证验证码
说明 : 调用此接口 ,传入手机号码和验证码, 可校验验证码是否正确 说明 : 调用此接口 ,传入手机号码和验证码, 可校验验证码是否正确
@@ -804,6 +826,8 @@ tags: 歌单标签
`lasttime` : 返回数据的 `lasttime` ,默认-1,传入上一次返回结果的 lasttime,将会返回下一页的数据 `lasttime` : 返回数据的 `lasttime` ,默认-1,传入上一次返回结果的 lasttime,将会返回下一页的数据
接口会传入官方客户端使用的 `fromRN=true`,登录用户读取自己的动态时可获得网易云允许本人查看的非公开动态。返回的 `size` 是网易云账户侧统计值,不保证等于分页后实际可获取的动态数量。
**接口地址 :** `/user/event` **接口地址 :** `/user/event`
**调用例子 :** `/user/event?uid=32953014` `/user/event?uid=32953014&limit=1&lasttime=1558011138743` **调用例子 :** `/user/event?uid=32953014` `/user/event?uid=32953014&limit=1&lasttime=1558011138743`
@@ -821,6 +845,38 @@ tags: 歌单标签
41、21 分享视频 41、21 分享视频
``` ```
### 获取当前登录用户的全部可枚举动态
说明 : 登录后调用此接口,会组合 `/user/account` 与 `/user/event` 的原子能力:先从 Cookie 取得当前用户 id,再自动跟随 `lasttime` 游标读取至 `more=false`。接口返回网易云允许当前用户本人查看的公开及非公开动态,不能读取其他用户的私密动态。
本接口不接受 `limit` 或 `lasttime`,一次请求会完成全部上游分页。账号动态较多时,请预留足够的请求时间。
数量字段说明:
- `size`:网易云返回的账户统计数量,可能包含已删除、被屏蔽、资源失效或旧类型且不再下发的记录。
- `retrievedCount`:本次实际取得的唯一动态数量,始终等于 `events.length`。
- `unavailableCount`:`size - retrievedCount` 的正差值;缺失记录没有返回 id,无法继续读取或修改。
- `sizeMismatch`:`size` 与 `retrievedCount` 是否不一致。若上游未返回 `size`,相关字段为 `null`。
- `pageCount`:本次实际请求的上游分页数量。
每条动态的 `privacySetting` 表示当前可见权限:`0` 为所有人,`1` 为我关注的人,`2` 为仅自己,`6` 为互相关注的人。
**接口地址 :** `/user/event/all`
**调用例子 :** `/user/event/all`
### 修改动态可见权限
说明 : 登录后调用此接口,可以修改当前账号本人发布的单条动态的可见权限。此接口只负责一次原子修改;上游没有批量修改接口。如需批量操作,可先调用 `/user/event/all` 并按 `privacySetting` 筛选,再由调用方逐条调用本接口,同时自行处理限速、失败重试和部分成功。
**必选参数 :** `evId` : 动态 id
`privacy` : 目标可见权限。`0` 为所有人,`1` 为我关注的人,`2` 为仅自己,`6` 为互相关注的人
**接口地址 :** `/event/privacy`
**调用例子 :** `/event/privacy?evId=6712917601&privacy=0`
### 转发用户动态 ### 转发用户动态
说明 : 登录后调用此接口 ,可以转发用户动态 说明 : 登录后调用此接口 ,可以转发用户动态
@@ -1248,12 +1304,14 @@ tags: 歌单标签
**必选参数 :** `id` : 音乐 id **必选参数 :** `id` : 音乐 id
`level`: 播放音质等级, 分为 `standard` => `标准`,`higher` => `较高`, `exhigh`=>`极高`, `level`: 播放音质等级, 分为 `standard` => `标准`,`higher` => `较高`, `exhigh`=>`极高`,
`lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清环绕声`, `sky` => `沉浸环绕声`, `dolby` => `杜比全景声`, `jymaster` => `超清母带` `lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清臻音`, `dolby` => `杜比全景声`, `vivid` => `臻音全景声`, `jymaster` => `超清母带`, `sky` => `沉浸环绕声`
`unblock`: 是否使用使用歌曲解锁, 分为`true`和`false` `unblock`: 是否使用使用歌曲解锁, 分为`true`和`false`
**可选参数 :** `immerseType`: 沉浸声环绕声类型, 分为`c512` => `新版c51类型`, `ste2` => `新版环绕立体声类型`, `aac2` => `新版aac类型`, `c51` => `c51类型`, `ste` => `环绕立体声类型`, `aac` => `aac类型`, 仅在 `level=sky` 时生效, 默认为 `c51`
**接口地址 :** `/song/url/v1` **接口地址 :** `/song/url/v1`
**调用例子 :** `/song/url/v1?id=1969519579&level=exhigh` `/song/url/v1?id=1969519579,33894312&level=lossless` **调用例子 :** `/song/url/v1?id=1969519579&level=exhigh` `/song/url/v1?id=1969519579,33894312&level=lossless` `/song/url/v1?id=1969519579&level=sky&immerseType=ste` `/song/url/v1?id=1969519579&level=vivid`
说明:`杜比全景声`音质需要设备支持,不同的设备可能会返回不同码率的 url。cookie 需要传入`os=pc`保证返回正常码率的 url。 说明:`杜比全景声`音质需要设备支持,不同的设备可能会返回不同码率的 url。cookie 需要传入`os=pc`保证返回正常码率的 url。
@@ -1263,7 +1321,7 @@ tags: 歌单标签
**必选参数 :** `id` : 音乐 id **必选参数 :** `id` : 音乐 id
`level`: 播放音质等级, 分为 `standard` => `标准`,`higher` => `较高`, `exhigh`=>`极高`, `level`: 播放音质等级, 分为 `standard` => `标准`,`higher` => `较高`, `exhigh`=>`极高`,
`lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清环绕声`, `sky` => `沉浸环绕声`, `dolby` => `杜比全景声`, `jymaster` => `超清母带` `lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清臻音`, `dolby` => `杜比全景声`, `vivid` => `臻音全景声`, `jymaster` => `超清母带`, `sky` => `沉浸环绕声`
`unblock`: 是否使用使用歌曲解锁, 分为`true`和`false` `unblock`: 是否使用使用歌曲解锁, 分为`true`和`false`
**接口地址 :** `/song/url/v1/302` **接口地址 :** `/song/url/v1/302`
@@ -1387,8 +1445,6 @@ tags: 歌单标签
说明 : 调用此接口 , 传入类型和歌单 id 可收藏歌单或者取消收藏歌单 说明 : 调用此接口 , 传入类型和歌单 id 可收藏歌单或者取消收藏歌单
!> 警告: 在`v4.29.7`版本后, 在网易云登陆后请求要带上`timestamp`字段, 否则会导致请求不合法
**必选参数 :** **必选参数 :**
`t` : 类型,1:收藏,2:取消收藏 `t` : 类型,1:收藏,2:取消收藏
@@ -1494,6 +1550,7 @@ tags: 歌单标签
- (可能存在)JSON 歌曲元数据 - (可能存在)JSON 歌曲元数据
``` ```
{"t":0,"c":[{"tx":"作曲: "},{"tx":"柳重言","li":"http://p1.music.126.net/Icj0IcaOjH2ZZpyAM-QGoQ==/6665239487822533.jpg","or":"orpheus://nm/artist/home?id=228547&type=artist"}]} {"t":0,"c":[{"tx":"作曲: "},{"tx":"柳重言","li":"http://p1.music.126.net/Icj0IcaOjH2ZZpyAM-QGoQ==/6665239487822533.jpg","or":"orpheus://nm/artist/home?id=228547&type=artist"}]}
{"t":5403,"c":[{"tx":"编曲: "},{"tx":"Alex San","li":"http://p1.music.126.net/pSbvYkrzZ1RFKqoh-fA9AQ==/109951166352922615.jpg","or":"orpheus://nm/artist/home?id=28984845&type=artist"}]} {"t":5403,"c":[{"tx":"编曲: "},{"tx":"Alex San","li":"http://p1.music.126.net/pSbvYkrzZ1RFKqoh-fA9AQ==/109951166352922615.jpg","or":"orpheus://nm/artist/home?id=28984845&type=artist"}]}
@@ -1510,6 +1567,7 @@ tags: 歌单标签
* 逐字歌词 * 逐字歌词
``` ```
[16210,3460](16210,670,0)还(16880,410,0)没... [16210,3460](16210,670,0)还(16880,410,0)没...
~~~~1 ~~~2 ~~~~3 ~~4 5 ~6 (...) ~~~~1 ~~~2 ~~~~3 ~~4 5 ~6 (...)
@@ -2333,6 +2391,16 @@ privilege:权限相关信息
**调用例子 :** `/recommend/songs` **调用例子 :** `/recommend/songs`
### 获取每日推荐歌曲 - v1
说明 : 调用此接口 , 可获得每日推荐歌曲 ( 需要登录 )
**可选参数 :** `ispush`: 是否刷新日推 , 默认为 false
**接口地址 :** `/recommend/songs/v1`
**调用例子 :** `/recommend/songs/v1`
### 每日推荐歌曲-不感兴趣 ### 每日推荐歌曲-不感兴趣
说明 : 日推歌曲标记为不感兴趣( 同时会返回一个新推荐歌曲, 需要登录 ) 说明 : 日推歌曲标记为不感兴趣( 同时会返回一个新推荐歌曲, 需要登录 )
@@ -2415,6 +2483,18 @@ privilege:权限相关信息
喜欢成功则返回数据的 code 为 200, 其余为失败 喜欢成功则返回数据的 code 为 200, 其余为失败
### 喜欢音乐 - 新版
说明: 调用此接口 , 传入音乐 id, 可喜欢该音乐
**必选参数 :** `id`: 歌曲 id
**可选参数 :** `like`: 布尔值 , 默认为 true 即喜欢 , 若传 false, 则取消喜欢
**接口地址 :** `/like/v1`
**调用例子 :** `/like/v1?id=347230`
### 喜欢音乐列表 ### 喜欢音乐列表
说明 : 调用此接口 , 传入用户 id, 可获取已喜欢音乐 id 列表(id 数组) 说明 : 调用此接口 , 传入用户 id, 可获取已喜欢音乐 id 列表(id 数组)
@@ -2490,6 +2570,30 @@ privilege:权限相关信息
**调用例子 :** `/scrobble?id=518066366&sourceid=36780169&time=291` **调用例子 :** `/scrobble?id=518066366&sourceid=36780169&time=291`
#### 听歌打卡 V2 (NCBL 加密版)
说明 : 调用此接口,使用桌面客户端 NCBL 加密日志格式上报听歌记录
**必选参数 :** `id`: 歌曲 id, `time`: 播放时长(秒)
**可选参数 :** `sourceid`: 来源列表 id, `source`: 来源名称(默认 list), `name`: 歌曲名, `artist`: 艺术家, `bitrate`: 码率(默认 320), `level`: 音质等级(默认 exhigh), `total`: 歌曲总时长(秒)
**接口地址 :** `/scrobble/v1`
**调用例子 :** `/scrobble/v1?id=518066366&sourceid=36780169&time=291`
### 提交歌曲播放状态
说明 : 调用此接口可提交歌曲播放状态,支持会话追踪和播放模式记录,未传入 `sessionId` 时后端会自动生成
**必选参数 :** `id`: 歌曲 id
**可选参数 :** `sessionId`: 播放会话 ID(12 位大写字母和数字),不传则自动生成, `progress`: 播放进度(秒),默认 0, `playMode`: 播放模式,默认 `list_loop`, `type`: 资源类型,默认 `song`
**接口地址 :** `/relay/play/state/submit`
**调用例子 :** `/relay/play/state/submit?id=518066366&progress=30`
### 热门歌手 ### 热门歌手
说明 : 调用此接口 , 可获取热门歌手数据 说明 : 调用此接口 , 可获取热门歌手数据
@@ -2871,6 +2975,7 @@ type : 地区
- 适合 Vercel、Netlify 等有请求体限制的平台 - 适合 Vercel、Netlify 等有请求体限制的平台
- 需要前端配合实现 - 需要前端配合实现
#### 客户端直传相关接口 #### 客户端直传相关接口
**获取上传凭证** **获取上传凭证**
@@ -2917,6 +3022,7 @@ type : 地区
- `artist`: 艺术家 - `artist`: 艺术家
- `album`: 专辑名 - `album`: 专辑名
#### 客户端直传流程 #### 客户端直传流程
1. 客户端计算文件 MD5 1. 客户端计算文件 MD5
@@ -2924,6 +3030,7 @@ type : 地区
3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl` 3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl`
4. 调用 `/cloud/upload/complete` 完成导入 4. 调用 `/cloud/upload/complete` 完成导入
### 云盘歌曲信息匹配纠正 ### 云盘歌曲信息匹配纠正
说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0 说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0
@@ -3540,6 +3647,36 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
**调用例子 :** `/yunbei/task/finish?userTaskId=5146243240&depositCode=0` **调用例子 :** `/yunbei/task/finish?userTaskId=5146243240&depositCode=0`
### 云贝广告任务 - 今日任务状态
说明 :登录后调用此接口可查询云贝广告任务("听歌/看视频得云贝")今日状态。逆向自云贝任务中心 H5 页面(st.music.163.com/yunbei-listen)。返回 `times`(今日已完成次数)、`amount`(今日累计云贝)、`singleAmount`(单次可得云贝)。单日上限 10 次。
**接口地址 :** `/yunbei/task/list/v1`
**调用例子 :** `/yunbei/task/list/v1`
### 云贝广告任务 - 获取推荐歌曲
说明 :登录后调用此接口可获取云贝广告任务的推荐歌曲列表。返回数组项含 `songId`、`songName`、`artistName`、`albumUrl`、`songChorusStartTime`、`likeFlag`、`alg`(均为 `alg_payrec_yunBei_*`)。
**可选参数 :** `offset`: 偏移数量,默认为 0
`limit`: 取出数量,默认为 10(客户端每次固定取 10 首)
**接口地址 :** `/yunbei/task/recommend/song`
**调用例子 :** `/yunbei/task/recommend/song` `/yunbei/task/recommend/song?offset=0&limit=10`
### 云贝广告任务 - 完成任务领取云贝
说明 :登录后调用此接口可完成任务并领取云贝。实测仅需传 `yunbeiAmount`(单次云贝数,客户端从 `list` 接口的 `singleAmount` 取值,当前为 150)即可成功领取,无需真实听歌/看视频。单日上限 10 次 × 150 = 1500 云贝/天,超限返回 `code:400 "单日完成任务数已达上限"`。建议领取前先调用 `/yunbei/task/list/v1` 查询今日剩余次数。
**可选参数 :** `yunbeiAmount`: 单次云贝数,默认为 150
**接口地址 :** `/yunbei/task/finish/v1`
**调用例子 :** `/yunbei/task/finish/v1?yunbeiAmount=150`
### 云贝收入 ### 云贝收入
说明 :登录后调用此接口可获取云贝收入 说明 :登录后调用此接口可获取云贝收入
@@ -3575,6 +3712,32 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
**调用例子 :** `/artist/new/song?limit=1` `/artist/new/song?limit=1&before=1602777625000` **调用例子 :** `/artist/new/song?limit=1` `/artist/new/song?limit=1&before=1602777625000`
### 关注歌手最近新歌 - 播放全部
说明 : 登录后调用此接口可获取所有关注歌手最近的 50 首新歌
**接口地址 :** `/artist/new/song/playall`
**调用例子 :** `/artist/new/song/playall`
### 关注歌手新作品(歌曲/MV)
说明 : 登录后调用此接口可分页获取关注歌手的新歌曲和 MV
**可选参数 :** `limit`: 返回数量,默认为 10
`startTimestamp`: 时间游标,首次请求默认为当前时间,后续请求传入上一页返回的时间游标
`before`: `startTimestamp` 的别名
`sourceType`: 作品来源类型,已确认 `1` 表示新歌,其他取值待确认,默认为 `1`
`firstRequest`: 是否首次请求,默认为 `true`,翻页时传 `false`
**接口地址 :** `/artist/new/song/mv/list/v2`
**调用例子 :** `/artist/new/song/mv/list/v2` `/artist/new/song/mv/list/v2?startTimestamp=1783680576099&sourceType=1&limit=10&firstRequest=false`
### 关注歌手新 MV ### 关注歌手新 MV
说明 :登录后调用此接口可获取关注歌手新 MV 说明 :登录后调用此接口可获取关注歌手新 MV
@@ -3705,6 +3868,14 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
**调用例子 :** `/vip/growthpoint/get?ids=7043206830_7` `/vip/growthpoint/get?ids=8613118351_1,8607552957_1` **调用例子 :** `/vip/growthpoint/get?ids=7043206830_7` `/vip/growthpoint/get?ids=8613118351_1,8607552957_1`
### 一键领取所有 vip 成长值
说明 : 登录后调用此接口 , 一键领取所有已完成的会员任务成长值奖励
**接口地址 :** `/vip/growthpoint/getall`
**调用例子 :** `/vip/growthpoint/getall`
### 歌手粉丝 ### 歌手粉丝
说明 : 调用此接口 , 传入歌手 id, 可获取歌手粉丝 说明 : 调用此接口 , 传入歌手 id, 可获取歌手粉丝
@@ -3989,6 +4160,16 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
**调用例子:** `/song/wiki/summary?id=1958384591` **调用例子:** `/song/wiki/summary?id=1958384591`
### 音乐百科
说明: 调用此接口可以获取歌曲的音乐百科信息
**接口地址:** `/song/wiki/info`
**必选参数:** `id`: 歌曲 ID
**调用例子:** `/song/wiki/info?id=1958384591`
### 乐谱列表 ### 乐谱列表
说明: 调用此接口可以获取歌曲的乐谱列表 说明: 调用此接口可以获取歌曲的乐谱列表
@@ -4196,6 +4377,7 @@ ONLINE 已发布
- `voiceFeeType: 0`:返回免费的声音 - `voiceFeeType: 0`:返回免费的声音
- `voiceFeeType: 1`:返回收费的声音 - `voiceFeeType: 1`:返回收费的声音
### 播客声音详情 ### 播客声音详情
说明: 获取播客里的声音详情 说明: 获取播客里的声音详情
@@ -4250,27 +4432,33 @@ ONLINE 已发布
### 播客上传声音 ### 播客上传声音
说明: 可以上传声音到播客,例子在 `/public/voice_upload.html` 访问地址: <a href="/voice_upload.html" target="_blank">/voice_upload.html</a> 说明: 登录后调用此接口,使用`'Content-Type': 'multipart/form-data'`上传声音文件 formData(name 为`songFile`),可通过 formData(name 为`imgFile`)同时上传声音封面。例子在 `/public/voice_upload.html` 访问地址: <a href="/voice_upload.html" target="_blank">/voice_upload.html</a>
**接口地址:** `/voice/upload` **接口地址:** `/voice/upload`
**必选参数:** **必选参数:**
`voiceListId`: 播客 id
`coverImgId`: 播客封面 `songFile`: 声音文件
`voiceListId`: 播客 id
`categoryId`: 分类 id `categoryId`: 分类 id
`secondCategoryId`:次级分类 id `secondCategoryId`: 次级分类 id
`description`: 声音介绍 `description`: 声音介绍
**可选参数:** **可选参数:**
`imgFile`: 声音封面图片文件,上传后会自动生成图片 id。与`coverImgId`同时传入时,优先使用`imgFile`
`coverImgId`: 已上传的声音封面图片 id,未传入`imgFile`时使用该值
`songName`: 声音名称 `songName`: 声音名称
`privacy`: 设为隐私声音,播客如果是隐私博客,则必须设为 1 `privacy`: 设为隐私声音,播客如果是隐私播客,则必须设为 1
`publishTime`:默认立即发布,定时发布的话需传入时间戳 `publishTime`: 默认立即发布,定时发布的话需传入时间戳
`autoPublish`: 是否发布动态,是则传入 1 `autoPublish`: 是否发布动态,是则传入 1
@@ -4497,7 +4685,7 @@ qrCodeStatus:20,detailReason:0 验证成功 qrCodeStatus:21,detailReason:0 二
### 歌曲音质详情 ### 歌曲音质详情
说明: 调用此接口获取歌曲各个音质的文件信息,与 `获取歌曲详情` 接口相比,多出 `高清环绕声`、`沉浸环绕声`、`超清母带`等音质的信息 说明: 调用此接口获取歌曲各个音质的文件信息,与 `获取歌曲详情` 接口相比,多出`高清臻音`、`杜比全景声`、`臻音全景声`、`超清母带`、`沉浸环绕声`等音质的信息
**必选参数:** **必选参数:**
@@ -4534,11 +4722,11 @@ qrCodeStatus:20,detailReason:0 验证成功 qrCodeStatus:21,detailReason:0 二
**必选参数:** **必选参数:**
`mode`: 模式 (aidj, DEFAULT, FAMILIAR, EXPLORE, SCENE_RCMD) `mode`: 模式 (`DEFAULT`: 默认模式,`FAMILIAR`: 熟悉模式,`EXPLORE`: 探索模式,`SCENE_RCMD`: 场景模式, 需要传入`submode`参数,`PUZZLE_MODE_RCMD`: 拼图模式)
**可选参数:** **可选参数:**
`submode`: 当 mode 为 SCENE_RCMD 是可为 ( EXERCISE, FOCUS, NIGHT_EMO ) `submode`: 当 mode 为 SCENE_RCMD 时传入 ( `EXERCISE`: 运动, `FOCUS`: 专注, `NIGHT_EMO`: 伤感, `SLEEP_HELP`: 助眠, `RELAX`: 放松, `CHEERFUL`: 欢快, `LYRICAL`: 抒情, `CURE`: 治愈, `SWEET`: 情歌, `RHYTHM_BLUES`: R&B, `RAINY`: 雨天, `GAMES`: 游戏, `RAP`: 说唱, `K_POP`: K-Pop, `ORIGINAL_MUSICIAL`: 宝藏原创, `ELECTRONIC`: 电音, `COMMUTE`: 出行, `TAKE_SHOWER`: 洗澡, `COFFEE_SHOP`: 咖啡馆, `ROCK`: 摇滚, `INSPIRATIONAL`: 励志, `CHINESE`: 华语, `ENGLISH`: 欧美, `YUEYU`: 粤语, `MANYAO`: 慢摇DJ, `JINGDIAN`: 经典, `LIGHT`: 轻音乐, `GUOFENG`: 国风, `FOLK`: 民谣, `ACG`: 二次元, `GUDIAN`: 古典, `JAZZ`: 爵士, `JAPANESE`: 日语, `GLOBAL`: 全球, `FRANCH`: 法语, `BLUE`: 蓝调, `DANCE`: 舞蹈, `LATIN`: 拉丁, `PUNK`: 放克, `COUNTRY`: 乡村乐, `MUSICAL`: 音乐剧, `YINGSHI`: 影视)
**接口地址:** `/personal/fm/mode` **接口地址:** `/personal/fm/mode`
@@ -4618,7 +4806,7 @@ bitrate = Math.floor(br / 1000)
**必选参数 :** `id` : 音乐 id **必选参数 :** `id` : 音乐 id
`level`: 播放音质等级, 分为 `standard` => `标准`,`higher` => `较高`, `exhigh`=>`极高`, `level`: 播放音质等级, 分为 `standard` => `标准`,`higher` => `较高`, `exhigh`=>`极高`,
`lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清环绕声`, `sky` => `沉浸环绕声`, `dolby` => `杜比全景声`, `jymaster` => `超清母带` `lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清臻音`, `dolby` => `杜比全景声`, `vivid` => `臻音全景声`, `jymaster` => `超清母带`, `sky` => `沉浸环绕声`
**接口地址 :** `/song/download/url/v1` **接口地址 :** `/song/download/url/v1`
@@ -4772,6 +4960,23 @@ bitrate = Math.floor(br / 1000)
**接口地址 :** `/listen/data/today/song` **接口地址 :** `/listen/data/today/song`
### 听歌足迹 - 歌曲播放排行
说明 : 登录后调用此接口, 获取歌曲播放排行, 返回每首歌的播放次数 (playCount); 默认返回前 20 名 (Top20), 不足则返回全部
**必选参数 :**
`type`: 维度类型 周 week 月 month
**可选参数 :**
`endTime` : 周: 每周周六 0 点的时间戳 月: 每月最后一天 0 点的时间戳
不填就是本周/月的
**接口地址 :** `/listen/data/song/play/rank`
**调用例子 :** `/listen/data/song/play/rank?type=month`
### 听歌足迹 - 总收听时长 ### 听歌足迹 - 总收听时长
说明 : 登录后调用此接口, 获取总收听时长; 相关接口可能需要 vip 权限 说明 : 登录后调用此接口, 获取总收听时长; 相关接口可能需要 vip 权限
@@ -5028,7 +5233,7 @@ let data = encodeURIComponent(
**调用例子:** `/vip/sign` **调用例子:** `/vip/sign`
### 黑胶乐签打卡信息 ### 黑胶乐签未来打卡信息
说明: 登录后调用此接口, 获取黑胶乐签打卡信息 说明: 登录后调用此接口, 获取黑胶乐签打卡信息
@@ -5050,7 +5255,6 @@ let data = encodeURIComponent(
**调用例子:** `/broadcast/sub?id=5&t=1` **调用例子:** `/broadcast/sub?id=5&t=1`
### 用户的创建歌单列表 ### 用户的创建歌单列表
说明 : 调用此接口, 传入用户id, 获取用户的创建歌单列表 说明 : 调用此接口, 传入用户id, 获取用户的创建歌单列表
@@ -5125,7 +5329,6 @@ let data = encodeURIComponent(
**调用例子 :** `/voicelist/my/created` **调用例子 :** `/voicelist/my/created`
### DIFM电台 - 分类 ### DIFM电台 - 分类
说明: 调用此接口, 获取DIFM电台分类 说明: 调用此接口, 获取DIFM电台分类
@@ -5304,7 +5507,7 @@ let data = encodeURIComponent(
**接口地址 :** `/comment/report` **接口地址 :** `/comment/report`
**调用例子 :* `/comment/report?id=2058263032&cid=123456789&reason=人身攻击` *_调用例子 :_ `/comment/report?id=2058263032&cid=123456789&reason=人身攻击`
### 多级行政区划数据 ### 多级行政区划数据
@@ -5336,6 +5539,431 @@ let data = encodeURIComponent(
说明 : 调用此接口,可获取城市榜、城市风格榜等指定维度音乐排行榜歌曲列表 说明 : 调用此接口,可获取城市榜、城市风格榜等指定维度音乐排行榜歌曲列表
**必选参数 :**
`chartCode`: 榜单编码,如 `CITY_SONG_CHART`、`CITY_STYLE_SONG_CHART`
`targetId`: 目标 id,城市榜如 `110000`,城市风格榜如 北京华语流行榜 `110000_1020`。城市风格榜格式通常为 `城市 id_曲风 id`,其中曲风 id 可通过[曲风列表](#曲风列表)接口 `/style/list` 获取。城市榜的城市列表可通过[多级行政区划数据](#多级行政区划数据)接口传入 `bizCode=chart` 获取;城市风格榜的城市列表可通过该接口传空 `bizCode` 获取
`targetType`: 目标类型,如 `CITY`、`CITY_STYLE`
**接口地址 :** `/chart/song/detail`
**调用例子 :** `/chart/song/detail?chartCode=CITY_STYLE_SONG_CHART&targetId=110000_1020&targetType=CITY_STYLE`
### 会员任务 - 新版
说明 : 登录后调用此接口, 获取会员任务
**可选参数** `id`: 用户 id, 传入后可获取指定用户的会员任务, 不传入则获取当前登录用户的会员任务
**接口地址 :** `/vip/task/v1`
**调用例子 :** `/vip/task/v1` `/vip/task/v1?id=32953014`
### 黑胶乐签详情
说明 : 登录后调用此接口, 传入时间戳, 获取黑胶乐签详情
**必选参数 :** `timestamp`: 时间戳, 单位毫秒, 如 `1704067200000` 表示 2024 年 12 月 31 日 0 点 (不传入会出现随机的乐签详情)
**接口地址 :** `/vip/sign/detail`
**调用例子 :** `/vip/sign/detail`
### 黑胶乐签历史
说明 : 登录后调用此接口, 获取黑胶乐签历史
**接口地址 :** `/vip/sign/history`
**调用例子 :** `/vip/sign/history`
### 直接获取云盘歌曲下载链接
说明 : 调用此接口, 传入云盘歌曲 id, 可直接获取云盘歌曲下载链接
**必选参数 :** `id`: 云盘歌曲 id
**接口地址 :** `/song/cloud/download`
**调用例子 :** `/song/cloud/download?id=123456789`'
### 获取广告
说明 : 调用此接口, 可获取广告
**接口地址 :** `/ad/get`
**调用例子 :** `/ad/get`
### 看广告领取权益(免费听歌时长 / 云贝等)
说明 : 登录后调用此接口, 领取广告权益。权益类型由广告平台下发的配置决定, 不仅限于 30 分钟免费听歌时长, 还包括"看视频获得最高 2000 云贝"等拉新分段权益(`rightsGainMethod=6`)。除下方常用参数外, 权益类型/时长/扩展权益等其余字段会自动从广告下发配置补齐, 无需手动传入。
!> 警告: 通过调取接口出现的任何问题由调用者自行承担
**可选参数 :**
`reqUid` : 广告请求 ID, 通过 `/ad/get` 获取, 未传时自动获取
`uid` : 当前登录用户 ID, 不传时服务端从 Cookie 识别
`rightsGainMethod` : 权益领取方式, `1`: 曝光, `2`: 曝光+点击(默认), `3`: 曝光+下载, `4`: 点击+停留, `5`: 曝光或点击停留, `6`: 拉新曝光/下载分段权益(看视频得云贝)
`type_ids` : 广告位类型, 默认 `["400002_0"]`
`creativeType` : 广告创意类型, 激励视频场景为 `36`, 默认 `36`
**接口地址 :** `/ad/listening/rights/gain`
**调用例子 :** `/ad/listening/rights/gain`
### 获取免费听时长状态
说明 : 登录后调用此接口, 获取免费听剩余时长
**接口地址 :** `/ad/listening/rights`
**调用例子 :** `/ad/listening/rights`
### 云小编 - 获取用户详情
说明: 登录后调用此接口, 获取云小编用户详情
**接口地址:** `/rep/ugc/user/get`
**调用例子:** `/rep/ugc/user/get`
### 云小编 - 每日签到
说明: 登录后调用此接口, 进行云小编签到, 领取 5 积分, 签到后 `/rep/ugc/user/get` 返回 `data.signed = 1`
**接口地址:** `/rep/ugc/user/sign`
**调用例子:** `/rep/ugc/user/sign`
### 云小编 - 查询会员任务状态
说明: 登录后调用此接口, 查询云小编会员任务状态, 当 `data.status = 20` 时, 可调用 `/rep/ugc/user/collect-vip` 领取会员
**接口地址:** `/rep/ugc/user/vip`
**调用例子:** `/rep/ugc/user/vip`
### 云小编 - 活动信息
说明: 登录后调用此接口, 查询云小编会员活动信息
**接口地址:** `/rep/ugc/activity/get`
**调用例子:** `/rep/ugc/activity/get`
### 云小编 - 获取任务
> 注意: 调用前请先使用官方客户端完成云小编“情绪标签审核”入站考试
**可选参数:**
`type`: 任务类型, 1: 歌曲曲风审核, 2: 歌曲语种审核, 3: 歌曲原唱审核, 4: 情绪标签审核, 默认 `4`
**接口地址:** `/thinktank/audit/resource/detail`
**调用例子:** `/thinktank/audit/resource/detail?type=4`
### 云小编 - 提交任务
> 注意: 投票结果会在后台审核, 一致 +3 积分, 不一致 -2 积分
**可选参数:**
`type`: 任务类型, 1: 歌曲曲风审核, 2: 歌曲语种审核, 3: 歌曲原唱审核, 4: 情绪标签审核, 默认 `4`
**必选参数:**
`taskId`: 任务 ID, 调用 `/thinktank/audit/resource/detail` 获取 `data.taskId`
`judgement`: 审核结果, 1: 同意, 2: 否决, 3: 跳过 (不算次数)
**接口地址:** `/thinktank/audit/resource/update`
**调用例子:** `/thinktank/audit/resource/update?type=4&taskId=123456&judgement=1`
### 云小编 - 领取任务积分
说明: 完成任务后调用此接口, 领取云小编任务积分
**可选参数:**
`activityId`: 活动 ID, 调用 `/rep/ugc/activity/get` 获取, 默认 `5001`
**接口地址:** `/rep/ugc/activity/collect`
**调用例子:** `/rep/ugc/activity/collect?activityId=5001`
### 云小编 - 领取一日会员
说明: 达成领取条件 (`/rep/ugc/user/vip`) 后调用此接口, 领取一日会员
**可选参数:**
`activityId`: 活动 ID, 调用 `/rep/ugc/activity/get` 获取, 默认 `5001`
**接口地址:** `/rep/ugc/user/collect-vip`
**调用例子:** `/rep/ugc/user/collect-vip?activityId=5001`
### 云小编 - 剩余抽奖次数
说明: 登录后调用此接口, 获取今日云小编抽奖剩余次数
**可选参数:**
`activityId`: 活动 ID, 默认 `6501202`
**接口地址:** `/middle/play/lottery/remain/chance`
**调用例子:** `/middle/play/lottery/remain/chance?activityId=6501202`
### 云小编 - 每日抽奖
说明: 登录后调用此接口, 消耗 200 积分进行抽奖, 每日最多抽 3 次
> 注意: 抽奖失败也消耗每日次数, 请先调用 `/rep/ugc/user/get` 查询可用积分
**可选参数:**
`activityId`: 活动 ID, 默认 `6501202`
`drawCount`: 未知, 默认 `1`
`checkToken`: 易盾反作弊 Token, 默认自动获取
**接口地址:** `/middle/play/do/lottery`
**调用例子:** `/middle/play/do/lottery?activityId=6501202&drawCount=1`
### 发送/删除评论
说明 : 调用此接口,可发送评论或者删除评论
1. 发送评论
**必选参数**
`type`: 数字,资源类型,对应歌曲,mv,专辑,歌单,电台,视频对应以下类型
```
0: 歌曲
1: mv
2: 歌单
3: 专辑
4: 电台
5: 视频
6: 动态
```
`id`: 对应资源 id
`content`: 要发送的内容
**调用例子** : `/comment/add?type=1&id=5436712&content=test` (往广岛之恋 mv 发送评论: test)
2. 回复评论
**必选参数**
`type`: 数字,资源类型,对应歌曲,mv,专辑,歌单,电台,视频对应以下类型
```
0: 歌曲
1: mv
2: 歌单
3: 专辑
4: 电台
5: 视频
6: 动态
```
`id`: 对应资源 id
`cid`: 评论 id
`content`: 要发送的内容
**调用例子** : `/comment/add?type=1&id=5436712&cid=1535550516319&content=test` (往广岛之恋 mv 回复test评论: test)
3. 删除评论
**必选参数**
`type`: 数字,资源类型,对应歌曲,mv,专辑,歌单,电台,视频对应以下类型
```
0: 歌曲
1: mv
2: 歌单
3: 专辑
4: 电台节目
5: 视频
6: 动态
7: 电台
```
`id`: 对应资源 id
`cid`: 评论 id
**调用例子** : `/comment?type=1&id=5436712&cid=1535550516319` (在广岛之恋 mv 删除评论)
### 获取在线设备列表
说明: 登录后调用此接口, 获取在线设备列表
**接口地址:** `/device/list`
**调用例子:** `/device/list`
### 发送安全验证码
说明: 登录后调用此接口, 传入手机号, 可发送安全验证码
**必选参数 :** `phone`: 手机号
**接口地址 :** `/captcha/safe/sent`
**调用例子 :** `/captcha/safe/sent?phone=13XXXXXXXXX`
### 强制下线设备
说明: 登录后调用此接口, 传入设备 id, 可强制下线设备的登录会话
**必选参数 :** `key`: 设备的 `deviceKey`, 可通过 `/device/list` 获取
`captcha`: 安全验证码, 可通过 `/captcha/safe/sent` 获取
**接口地址 :** `/device/kickoff`
**调用例子 :** `/device/kickoff?key=00ALDFGEXXXXXXXXXXXXXXXXX&captcha=1234`
### 上报设备中心设备名称
说明: 登录后调用此接口, 传入 `deviceName`, 可修改当前设备在网易云登录设备管理列表里显示的名称
**必选参数 :** `deviceName`: 期望展示的设备名称
`name`: `deviceName` 的兼容别名, 二选一即可
**接口地址 :** `/deviceinfo/center/upload`
**调用例子 :** `/deviceinfo/center/upload?deviceName=XXXXX`
### 插播相似歌曲
说明: 调用此接口, 传入歌曲 id, 可获取插播相似歌曲
**必选参数 :** `id`: 歌曲 id
**接口地址 :** `/song/simi/get`
**调用例子 :** `/song/simi/get?id=39227633`
### 获取用户乐迷团列表
说明 : 登录后调用此接口 , 可获取当前登录用户已加入的全部歌手乐迷团列表
**接口地址 :** `/fans/group/user/groups`
**调用例子 :** `/fans/group/user/groups`
### 获取乐迷团详情
说明 : 调用此接口 , 可获取乐迷团详情
**必选参数 :** `groupId` : 乐迷团 ID
**可选参数 :** `scene` : 场景标识
**接口地址 :** `/fans/group/detail`
**调用例子 :** `/fans/group/detail?groupId=1872529203038486609`
### 获取用户在乐迷团的详情
说明 : 登录后调用此接口 , 可获取当前用户在指定乐迷团的详细成员信息及铭牌等级
**必选参数 :** `groupId` : 乐迷团 ID
**接口地址 :** `/fans/group/user/group/detail`
**调用例子 :** `/fans/group/user/group/detail?groupId=1755777360858267711`
### 获取乐迷团推荐笔记
说明 : 调用此接口 , 可获取指定乐迷团的推荐笔记
**必选参数 :** `fansGroupId` : 乐迷团 ID
**可选参数 :** `cursor` : 游标 , 默认为 0
`size` : 返回数量 , 默认为 10
**接口地址 :** `/fans/group/feed/recommend`
**调用例子 :** `/fans/group/feed/recommend?fansGroupId=1872529203038486609&size=10&cursor=0`
### 每日指定风格歌曲推荐
说明 : 调用此接口 , 可获取每日指定风格歌曲推荐
**接口地址 :** `/recommend/category/songs`
**调用例子 :** `/recommend/category/songs`
### 获取每日指定风格歌曲推荐的风格列表
说明 : 调用此接口 , 可获取每日指定风格歌曲推荐的风格列表
**接口地址 :** `/recommend/category/configs`
**调用例子 :** `/recommend/category/configs`
### 设置每日指定风格歌曲推荐的风格
说明 : 调用此接口 , 可设置每日指定风格歌曲推荐的风格
**必选参数 :**
`tags`: 风格标签, 可通过 `/recommend/category/configs` 获取
`category`: 风格标签的父ID, 可通过 `/recommend/category/configs` 获取
!> 只能设置当前CategoryId下的tags, 不能设置其他CategoryId下的tags, 否则会报错
**接口地址 :** `/recommend/category/config`
**调用例子 :** `/recommend/category/config?tags=10004,10015&categoryId=1000`
## 离线访问此文档 ## 离线访问此文档
此文档同时也是 Progressive Web Apps(PWA), 加入了 serviceWorker, 可离线访问 此文档同时也是 Progressive Web Apps(PWA), 加入了 serviceWorker, 可离线访问

Some files were not shown because too many files have changed in this diff Show More