Compare commits

..
146 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
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
122 changed files with 10013 additions and 815 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 }}
@@ -260,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
+2 -1
View File
@@ -9,4 +9,5 @@ bin
anonymous_token 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` 白名单;改依赖安装行为时先看它。
+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
+359 -24
View File
@@ -37,6 +37,41 @@ export const enum SubAction {
unsub = 0, unsub = 0,
} }
export function ad_get(
params: { type_ids?: string } & RequestBaseConfig,
): Promise<Response>
export function ad_listening_rights(
params: RequestBaseConfig,
): Promise<Response>
export function ad_listening_rights_gain(
params: {
reqUid?: string
uid?: string | number
exposureTime?: string | number
clickTime?: string | number
extraRightsType?: string | number
playContinuously?: boolean | string
source?: string | number
creativeType?: string | number
rightsGainMethod?: string | number
extraRightsGainMethod?: string | number
extraRightsGainDuration?: string | number
nextRightsGainDuration?: string | number
rightsGainType?: string | number
rightsGainDuration?: string | number
gainMethodStep?: string | number
generalRightsInfo?: string
rightsExtJson?: string
appInfo?: string
contextInfo?: string
installed?: string | number
sniffTime?: string | number
type_ids?: string
} & RequestBaseConfig,
): Promise<Response>
export function activate_init_profile( export function activate_init_profile(
params: { nickname: string } & RequestBaseConfig, params: { nickname: string } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
@@ -119,6 +154,14 @@ export function album_sublist(
params: MultiPageConfig & RequestBaseConfig, params: MultiPageConfig & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function api(
params: {
uri: string
data?: string | Record<string, unknown>
crypto?: string
} & RequestBaseConfig,
): Promise<Response>
export function artist_album( export function artist_album(
params: { id: string | number } & MultiPageConfig & RequestBaseConfig, params: { id: string | number } & MultiPageConfig & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
@@ -271,10 +314,21 @@ export function batch(
params: { [index: string]: unknown } & RequestBaseConfig, params: { [index: string]: unknown } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function captcha_safe_sent(
params: { ctcode?: number | string } & RequestBaseConfig,
): Promise<Response>
export function captcha_sent( export function captcha_sent(
params: { phone: string; ctcode?: number | string } & RequestBaseConfig, params: { phone: string; ctcode?: number | string } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function captcha_sent_v1(
params: {
phone: number | string
ctcode?: number | string
} & RequestBaseConfig,
): Promise<Response>
export function captcha_verify( export function captcha_verify(
params: { params: {
ctcode?: number | string ctcode?: number | string
@@ -387,6 +441,14 @@ export function comment(
} & RequestBaseConfig, } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function comment_add(
params: {
id: string | number
type: CommentType
content: string
} & RequestBaseConfig,
): Promise<Response>
export function comment_album( export function comment_album(
params: { params: {
id: string | number id: string | number
@@ -395,6 +457,14 @@ export function comment_album(
RequestBaseConfig, RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function comment_delete(
params: {
id: string | number
type: CommentType
cid: string | number
} & RequestBaseConfig,
): Promise<Response>
export function comment_dj( export function comment_dj(
params: { params: {
id: string | number id: string | number
@@ -469,6 +539,15 @@ export function comment_playlist(
RequestBaseConfig, RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function comment_reply(
params: {
id: string | number
type: CommentType
cid: string | number
content: string
} & RequestBaseConfig,
): Promise<Response>
export function comment_video( export function comment_video(
params: { params: {
id: string | number id: string | number
@@ -490,6 +569,25 @@ export function daily_signin(
params: { type?: DailySigninType } & RequestBaseConfig, params: { type?: DailySigninType } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function decrypt(
params: {
crypto?: string
data?: string
hexString?: string
isReq?: boolean | string
} & RequestBaseConfig,
): Promise<Response>
export function device_kickoff(
params: { deviceKey: string | number; captcha?: string } & RequestBaseConfig,
): Promise<Response>
export function device_list(params: RequestBaseConfig): Promise<Response>
export function deviceinfo_center_upload(
params: { deviceName: string; name?: string } & RequestBaseConfig,
): Promise<Response>
export function digitalAlbum_ordering( export function digitalAlbum_ordering(
params: { params: {
payment: string payment: string
@@ -618,6 +716,10 @@ export function dj_toplist_popular(
params: { limit?: string | number } & RequestBaseConfig, params: { limit?: string | number } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function eapi_decrypt(
params: { hexString: string; isReq?: boolean | string } & RequestBaseConfig,
): Promise<Response>
export function event( export function event(
params: { pagesize?: number; lasttime?: number } & RequestBaseConfig, params: { pagesize?: number; lasttime?: number } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
@@ -626,6 +728,20 @@ export function event_del(
params: { evId: string | number } & RequestBaseConfig, params: { evId: string | number } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export const enum EventPrivacy {
everyone = 0,
following = 1,
onlyMe = 2,
mutualFollowing = 6,
}
export function event_privacy(
params: {
evId: string | number
privacy: EventPrivacy
} & RequestBaseConfig,
): Promise<Response>
export function event_forward( export function event_forward(
params: { params: {
forwords: string forwords: string
@@ -665,6 +781,8 @@ export function hot_topic(
params: MultiPageConfig & RequestBaseConfig, params: MultiPageConfig & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function inner_version(params: RequestBaseConfig): Promise<Response>
export function like( export function like(
params: { params: {
like?: 'true' | 'false' | boolean like?: 'true' | 'false' | boolean
@@ -678,6 +796,10 @@ export function likelist(
params: { uid: string | number } & RequestBaseConfig, params: { uid: string | number } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function listentogether_status(
params: RequestBaseConfig,
): Promise<Response>
export function login( export function login(
params: { email: string; password: string } & RequestBaseConfig, params: { email: string; password: string } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
@@ -724,6 +846,17 @@ export function lyric_new(
params: { id: string | number } & RequestBaseConfig, params: { id: string | number } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function middle_play_do_lottery(
params: {
activityId?: string | number
drawCount?: string | number
} & RequestBaseConfig,
): Promise<Response>
export function middle_play_lottery_remain_chance(
params: { activityId?: string | number } & RequestBaseConfig,
): Promise<Response>
export function msg_comments( export function msg_comments(
params: { params: {
uid: string | number uid: string | number
@@ -946,6 +1079,22 @@ export function register_cellphone(
} & RequestBaseConfig, } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function register_checktoken_v2(
params: { refresh?: boolean | string } & RequestBaseConfig,
): Promise<Response>
export function register_checktoken_v3(
params: { refresh?: boolean | string } & RequestBaseConfig,
): Promise<Response>
export function register_neapikey(
params: { version?: string | number; behavior?: string } & RequestBaseConfig,
): Promise<Response>
export function register_xeapikey(
params: { deviceId?: string; currentKeyVersion?: string } & RequestBaseConfig,
): Promise<Response>
export function related_allvideo( export function related_allvideo(
params: { id: string | number } & RequestBaseConfig, params: { id: string | number } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
@@ -954,6 +1103,67 @@ export function related_playlist(
params: { id: string | number } & RequestBaseConfig, params: { id: string | number } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function relay_play_state_submit(
params: {
id: string | number
sessionId?: string
progress?: string | number
playMode?: string
type?: string
} & RequestBaseConfig,
): Promise<Response>
export function rep_ugc_activity_collect(
params: { activityId?: string | number } & RequestBaseConfig,
): Promise<Response>
export function rep_ugc_activity_get(
params: RequestBaseConfig,
): Promise<Response>
export function rep_ugc_exam_info_get(
params: { examType: string | number } & RequestBaseConfig,
): Promise<Response>
export function rep_ugc_exam_question_single_get(
params: {
examType: string | number
taskId: string | number
} & RequestBaseConfig,
): Promise<Response>
export function rep_ugc_exam_result_get(
params: {
examType: string | number
taskId: string | number
} & RequestBaseConfig,
): Promise<Response>
export function rep_ugc_exam_start(
params: { examType: string | number } & RequestBaseConfig,
): Promise<Response>
export function rep_ugc_exam_submit(
params: {
examType: string | number
taskId: string | number
questionId: string | number
answer: string
} & RequestBaseConfig,
): Promise<Response>
declare const rep_ugc_user_collect_vip: (
params: { activityId?: string | number } & RequestBaseConfig,
) => Promise<Response>
export { rep_ugc_user_collect_vip as 'rep_ugc_user_collect-vip' }
export function rep_ugc_user_get(params: RequestBaseConfig): Promise<Response>
export function rep_ugc_user_sign(params: RequestBaseConfig): Promise<Response>
export function rep_ugc_user_vip(params: RequestBaseConfig): Promise<Response>
export const enum ResourceType { export const enum ResourceType {
mv = 1, mv = 1,
dj = 4, dj = 4,
@@ -979,6 +1189,22 @@ export function scrobble(
} & RequestBaseConfig, } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function scrobble_v1(
params: {
id: string | number
time: string | number
total?: string | number
sourceid?: string | number
sourceId?: string | number
source?: string
name?: string
artist?: string
bitrate?: string | number
level?: string
vip?: boolean | string
} & RequestBaseConfig,
): Promise<Response>
export function search( export function search(
params: { params: {
keywords: string keywords: string
@@ -1160,6 +1386,7 @@ export const enum SoundQualityType {
jyeffect = 'jyeffect', jyeffect = 'jyeffect',
jymaster = 'jymaster', jymaster = 'jymaster',
sky = 'sky', sky = 'sky',
vivid = 'vivid',
} }
export function song_url_v1( export function song_url_v1(
@@ -1267,6 +1494,25 @@ export function user_event(
} & RequestBaseConfig, } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export interface UserEventAllResponse {
code: number
events: Record<string, unknown>[]
/** Upstream account statistic; it can include events no longer returned. */
size: number | null
/** Number of unique events in `events`. */
retrievedCount: number
/** Positive difference between `size` and `retrievedCount`. */
unavailableCount: number | null
sizeMismatch: boolean | null
pageCount: number
more: false
lasttime: string | number | null
}
export function user_event_all(
params: RequestBaseConfig,
): Promise<Response<UserEventAllResponse>>
export function user_followeds( export function user_followeds(
params: { params: {
uid: string | number uid: string | number
@@ -1433,6 +1679,23 @@ export function yunbei_task_finish(
} & RequestBaseConfig, } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function yunbei_ad_task_list(
params: RequestBaseConfig,
): Promise<Response>
export function yunbei_ad_task_recommend_song(
params: {
offset?: number | string
limit?: number | string
} & RequestBaseConfig,
): Promise<Response>
export function yunbei_ad_task_finish(
params: {
yunbeiAmount?: number | string
} & RequestBaseConfig,
): Promise<Response>
export function msg_recentcontact(params: RequestBaseConfig): Promise<Response> export function msg_recentcontact(params: RequestBaseConfig): Promise<Response>
export function hug_comment( export function hug_comment(
@@ -1474,6 +1737,20 @@ export function artist_new_song(
} & RequestBaseConfig, } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function artist_new_song_playall(
params: RequestBaseConfig,
): Promise<Response>
export function artist_new_song_mv_list_v2(
params: {
startTimestamp?: number | string
before?: number | string
sourceType?: number | string
limit?: number | string
firstRequest?: boolean | string
} & RequestBaseConfig,
): Promise<Response>
export function artist_detail( export function artist_detail(
params: { params: {
id: number | string id: number | string
@@ -1594,6 +1871,10 @@ export function vip_growthpoint_get(
} & RequestBaseConfig, } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function vip_growthpoint_getall(
params: RequestBaseConfig,
): Promise<Response>
export function artist_fans( export function artist_fans(
params: { id: number | string } & MultiPageConfig & RequestBaseConfig, params: { id: number | string } & MultiPageConfig & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
@@ -2049,13 +2330,18 @@ export function listen_data_report(
} & RequestBaseConfig, } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function listen_data_song_play_rank(
params: {
type?: string
endTime?: string | number
} & RequestBaseConfig,
): Promise<Response>
export function listen_data_today_song( export function listen_data_today_song(
params: RequestBaseConfig, params: RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function listen_data_total( export function listen_data_total(params: RequestBaseConfig): Promise<Response>
params: RequestBaseConfig,
): Promise<Response>
export function listen_data_year_report( export function listen_data_year_report(
params: RequestBaseConfig, params: RequestBaseConfig,
@@ -2192,9 +2478,7 @@ export function radio_sport_get(
} & RequestBaseConfig, } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function recent_listen_list( export function recent_listen_list(params: RequestBaseConfig): Promise<Response>
params: RequestBaseConfig,
): Promise<Response>
export function recommend_songs_dislike( export function recommend_songs_dislike(
params: { params: {
@@ -2202,9 +2486,7 @@ export function recommend_songs_dislike(
} & RequestBaseConfig, } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function register_anonimous( export function register_anonimous(params: RequestBaseConfig): Promise<Response>
params: RequestBaseConfig,
): Promise<Response>
export function sati_resource_list( export function sati_resource_list(
params: { params: {
@@ -2229,9 +2511,7 @@ export function sati_resource_sub_list(
params: RequestBaseConfig, params: RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function sati_tag_list( export function sati_tag_list(params: RequestBaseConfig): Promise<Response>
params: RequestBaseConfig,
): Promise<Response>
export function sati_timescene_resources_get( export function sati_timescene_resources_get(
params: RequestBaseConfig, params: RequestBaseConfig,
@@ -2275,6 +2555,10 @@ export function song_chorus(
} & RequestBaseConfig, } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function song_cloud_download(
params: { id: string | number } & RequestBaseConfig,
): Promise<Response>
export function song_creators( export function song_creators(
params: { params: {
id: string | number id: string | number
@@ -2363,9 +2647,7 @@ export function song_url_match(
} & RequestBaseConfig, } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function song_url_ncmget( export function song_url_ncmget(params: RequestBaseConfig): Promise<Response>
params: RequestBaseConfig,
): Promise<Response>
export function song_url_v1_302( export function song_url_v1_302(
params: { params: {
@@ -2388,10 +2670,20 @@ export function threshold_detail_get(
params: RequestBaseConfig, params: RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function toplist_detail_v2( export function thinktank_audit_resource_detail(
params: RequestBaseConfig, params: { type?: string | number } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function thinktank_audit_resource_update(
params: {
type?: string | number
taskId: string | number
judgement: string | number
} & RequestBaseConfig,
): Promise<Response>
export function toplist_detail_v2(params: RequestBaseConfig): Promise<Response>
export function ugc_album_get( export function ugc_album_get(
params: { params: {
id: string | number id: string | number
@@ -2433,9 +2725,7 @@ export function ugc_song_get(
} & RequestBaseConfig, } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function ugc_user_devote( export function ugc_user_devote(params: RequestBaseConfig): Promise<Response>
params: RequestBaseConfig,
): Promise<Response>
export function user_detail_new( export function user_detail_new(
params: { params: {
@@ -2516,14 +2806,18 @@ export function verify_qrcodestatus(
} & RequestBaseConfig, } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function vip_sign( export function vip_sign(params: RequestBaseConfig): Promise<Response>
params: RequestBaseConfig,
export function vip_sign_detail(
params: { timestamp: string | number } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function vip_sign_info( export function vip_sign_history(
params: RequestBaseConfig, params: { type?: string | number } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function vip_sign_info(params: RequestBaseConfig): Promise<Response>
export function vip_tasks_v1( export function vip_tasks_v1(
params: { params: {
id?: string | number id?: string | number
@@ -2542,6 +2836,22 @@ export function voice_upload(
name: string name: string
data: string | Buffer data: string | Buffer
} }
imgFile?: {
name: string
data: string | Buffer
}
voiceListId: string | number
coverImgId?: string | number
categoryId: string | number
secondCategoryId: string | number
description: string
songName?: string
privacy?: string | number
publishTime?: string | number
autoPublish?: string | number
autoPublishText?: string
orderNo?: string | number
composedSongs?: string
} & RequestBaseConfig, } & RequestBaseConfig,
): Promise<Response> ): Promise<Response>
@@ -2580,3 +2890,28 @@ export function voicelist_trans(
} & MultiPageConfig & } & MultiPageConfig &
RequestBaseConfig, RequestBaseConfig,
): Promise<Response> ): Promise<Response>
export function fans_group_detail(
params: {
groupId: string
scene?: string
} & RequestBaseConfig,
): Promise<Response>
export function fans_group_feed_recommend(
params: {
fansGroupId: string
cursor?: string
size?: string | number
} & RequestBaseConfig,
): Promise<Response>
export function fans_group_user_groups(
params?: RequestBaseConfig,
): Promise<Response>
export function fans_group_user_group_detail(
params: {
groupId: string | number
} & RequestBaseConfig,
): Promise<Response>
+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'),
)
}
+34 -5
View File
@@ -1,11 +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( const results = {}
`/api/vip-center-bff/task/sign`,
data, const taskSign = await request(
'/api/vip-center-bff/task/sign',
{},
createOption(query, 'weapi'), 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,
}
} }
+4 -3
View File
@@ -1,13 +1,14 @@
// 黑胶乐签打卡历史 // 黑胶乐签打卡历史 / 状态查询
// 支持传入 type=0(用户信息栏)或 type=1(黑胶乐签)
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
module.exports = (query, request) => { module.exports = (query, request) => {
const data = { const data = {
type: '0', type: query.type || '0',
} }
return request( return request(
`/api/vipnewcenter/app/minidesk/music/sign/pc`, `/api/vipnewcenter/app/minidesk/music/sign/pc`,
data, data,
createOption(query, 'xeapi'), createOption(query, 'eapi'),
) )
} }
+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()
+24 -20
View File
@@ -1,7 +1,7 @@
{ {
"name": "@neteasecloudmusicapienhanced/api", "name": "@neteasecloudmusicapienhanced/api",
"version": "4.34.3", "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": {
@@ -73,48 +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": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://registry.npmjs.org/" "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.

Before

Width:  |  Height:  |  Size: 14 KiB

+632 -46
View File
File diff suppressed because it is too large Load Diff
+496
View File
@@ -0,0 +1,496 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>免费听 — 网易云音乐 API Enhanced</title>
<style>
:root {
--fg: #333;
--muted: #666;
--border: #ddd;
--bg: #f5f5f5;
--panel: #ffffff;
--accent: #333;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; }
.container { max-width: 800px; margin: 40px auto; padding: 0 20px; }
@media (max-width: 480px) {
.container { margin: 20px auto; padding: 0 16px; }
header.site-header h1 { font-size: 22px; }
.block { padding: 16px; }
}
header.site-header { margin-bottom: 24px; }
header.site-header h1 { font-size: 24px; font-weight: 600; margin: 0; }
.sub { margin-top: 4px; color: var(--muted); font-size: 14px; }
.block { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); }
.block h3 { margin: 0 0 4px; font-size: 15px; font-weight: 600; }
.block .desc { margin: 0 0 12px; color: var(--muted); font-size: 13px; }
pre { margin: 0; background: #f9f9f9; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; font-family: 'Courier New', monospace; font-size: 13px; max-height: 200px; overflow: auto; }
pre.data { max-height: 360px; }
.row { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
button { padding: 8px 18px; border-radius: 8px; border: 1px solid var(--border); font-size: 14px; font-weight: 500; cursor: pointer; background: var(--panel); color: var(--fg); transition: all .15s; }
button:hover { background: #eee; }
button.primary { background: var(--fg); color: var(--panel); border-color: var(--fg); }
button.primary:hover { opacity: .85; }
button:disabled { opacity: .4; cursor: not-allowed; }
button.danger { background: #a11; color: #fff; border-color: #a11; }
button.danger:hover { opacity: .85; }
.tag { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 12px; font-weight: 500; border: 1px solid var(--border); }
.tag.ok { background: #e6f7e6; color: #1a7a1a; border-color: #b7e6b7; }
.tag.no { background: #ffe6e6; color: #a11; border-color: #f5c6c6; }
.tag.busy { background: #fff3cd; color: #856404; border-color: #ffeeba; }
.flex { display: flex; align-items: center; gap: 10px; }
.flex.sb { justify-content: space-between; }
.step { display: flex; gap: 12px; align-items: flex-start; }
.step .num { width: 26px; height: 26px; border-radius: 50%; background: var(--fg); color: var(--panel); display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; flex-shrink: 0; margin-top: 1px; }
.step .num.done { background: #1a7a1a; }
.step .body { flex: 1; min-width: 0; }
input[type=text] { width: 100%; padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border); font-size: 14px; font-family: inherit; margin-top: 6px; }
input[type=text]:focus { outline: 2px solid var(--fg); outline-offset: -1px; border-color: transparent; }
footer.site-footer { margin-top: 32px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); text-align: center; font-size: 13px; }
footer.site-footer a { color: var(--fg); text-decoration: none; }
footer.site-footer a:hover { border-bottom: 1px dotted var(--fg); }
.info-grid { display: grid; grid-template-columns: 80px 1fr; gap: 4px 12px; font-size: 13px; margin-top: 8px; }
.info-grid .lbl { color: var(--muted); }
/* ---- 剩余时长计时器 ---- */
.timer-wrap { text-align: center; padding: 12px 0 4px; }
.timer-row { display: flex; align-items: baseline; justify-content: center; gap: 2px; font-variant-numeric: tabular-nums; }
.timer-num { font-size: 36px; font-weight: 700; letter-spacing: 1px; line-height: 1.2; font-family: 'SF Mono', 'Courier New', monospace; min-width: 2ch; text-align: center; }
.timer-label { font-size: 13px; color: var(--muted); margin-right: 12px; }
.timer-label:last-child { margin-right: 0; }
.timer-sep { font-size: 28px; font-weight: 300; color: #bbb; margin: 0 2px; }
.timer-unit { display: flex; flex-direction: column; align-items: center; }
</style>
</head>
<body>
<main class="container">
<header class="site-header">
<h1>🎧 免费听 · 看广告获取时长</h1>
<p class="sub">模拟 App 内看激励视频广告 → 领取免费听权益的完整流程</p>
</header>
<!-- 步骤 1: 注册 Token -->
<div class="block">
<div class="step">
<div class="num" id="s1num">1</div>
<div class="body">
<div class="flex sb">
<h3>注册反作弊 Token</h3>
<span id="s1tag" class="tag no">未开始</span>
</div>
<p class="desc">调用易盾接口获取实时 token,广告接口需要此 token 才能正常响应</p>
<pre id="s1pre">(尚未执行)</pre>
<div class="row">
<button id="step1" class="primary">① 注册 Token</button>
</div>
</div>
</div>
</div>
<!-- 步骤 2: 获取广告 -->
<div class="block">
<div class="step">
<div class="num" id="s2num">2</div>
<div class="body">
<div class="flex sb">
<h3>获取广告 &amp; 提取 reqId</h3>
<span id="s2tag" class="tag no">未开始</span>
</div>
<p class="desc">请求广告位,从返回的 extJson.contextInfo.req_id 提取用于领取权益的标识</p>
<pre id="s2pre" class="data">(尚未执行)</pre>
<div class="row">
<button id="step2" class="primary" disabled>② 获取广告</button>
<button id="step2b" disabled>换一个广告位</button>
</div>
<div class="info-grid" id="s2info" style="display:none">
<div class="lbl">广告 ID</div><div id="s2adId">-</div>
<div class="lbl">reqId</div><div id="s2reqId">-</div>
<div class="lbl">文案</div><div id="s2text">-</div>
</div>
</div>
</div>
</div>
<!-- 步骤 3: 领取权益 -->
<div class="block">
<div class="step">
<div class="num" id="s3num">3</div>
<div class="body">
<div class="flex sb">
<h3>领取免费听权益</h3>
<span id="s3tag" class="tag no">未开始</span>
</div>
<p class="desc">传入 reqId 及相关参数,领取免费听权益(模拟点击广告后的领取请求)</p>
<pre id="s3pre" class="data">(尚未执行)</pre>
<div class="row">
<button id="step3" class="primary" disabled>③ 领取权益</button>
</div>
<div class="info-grid" id="s3info" style="display:none">
<div class="lbl">结果</div><div id="s3result">-</div>
</div>
</div>
</div>
</div>
<!-- 一键全流程 -->
<div class="block">
<div class="flex sb">
<h3>一键运行</h3>
<span id="allTag" class="tag no">就绪</span>
</div>
<p class="desc" style="margin-top:4px">按顺序自动执行以上三个步骤</p>
<div class="row">
<button id="runAll" class="primary">▶ 一键获取免费听时长</button>
<button id="clearAll" class="danger">✕ 清空</button>
</div>
</div>
<!-- 剩余时长计时器 -->
<div class="block" id="rightsBlock">
<div class="flex sb">
<h3 id="rightsTitleDisplay">🕐 免费听时长剩余</h3>
<span id="rightsTag" class="tag no">未查询</span>
</div>
<p class="desc" id="rightsSubDesc">查询你的免费听权益剩余时长</p>
<div class="timer-wrap">
<div class="timer-row" id="timerRow">
<div class="timer-unit">
<span class="timer-num" id="timerDays">--</span>
<span class="timer-label">天</span>
</div>
<span class="timer-sep">:</span>
<div class="timer-unit">
<span class="timer-num" id="timerHours">--</span>
<span class="timer-label">时</span>
</div>
<span class="timer-sep">:</span>
<div class="timer-unit">
<span class="timer-num" id="timerMinutes">--</span>
<span class="timer-label">分</span>
</div>
<span class="timer-sep">:</span>
<div class="timer-unit">
<span class="timer-num" id="timerSeconds">--</span>
<span class="timer-label">秒</span>
</div>
</div>
</div>
<div class="row" style="justify-content:center">
<button id="queryRightsBtn" class="primary">⟳ 查询剩余时长</button>
<button id="claim30Btn">领取30分钟</button>
</div>
<div class="info-grid" id="rightsInfo" style="display:none">
<div class="lbl">状态</div><div id="rightsStatus">-</div>
<div class="lbl">结束时间</div><div id="rightsEndTimeDisplay">-</div>
<div class="lbl">今日已覆盖</div><div id="rightsCoverToday">-</div>
</div>
</div>
<footer class="site-footer">
<a href="/">← 返回首页</a>
</footer>
</main>
<script>
const $ = (id) => document.getElementById(id)
const base = location.origin
function setTag(id, type, text) {
const el = $(id)
el.className = 'tag ' + type
el.textContent = text
}
function setNum(id, done) {
$(id).className = 'num' + (done ? ' done' : '')
}
function nocache(url) {
const sep = url.indexOf('?') > -1 ? '&' : '?'
return url + sep + '_t=' + Date.now()
}
// ===== 步骤 1: 注册 Token =====
async function step1() {
const btn = $('step1')
btn.disabled = true
setTag('s1tag', 'busy', '注册中…')
setNum('s1num', false)
try {
const res = await fetch(nocache(base + '/register/checktoken/v3?refresh=1'))
const data = await res.json()
$('s1pre').textContent = JSON.stringify(data, null, 2)
if (data.token) {
setTag('s1tag', 'ok', '已注册')
setNum('s1num', true)
$('step2').disabled = false
$('step2b').disabled = false
return data.token
}
setTag('s1tag', 'no', '失败')
return null
} catch (e) {
$('s1pre').textContent = JSON.stringify({ error: e.message }, null, 2)
setTag('s1tag', 'no', e.message)
return null
} finally {
btn.disabled = false
}
}
// ===== 步骤 2: 获取广告 =====
async function step2(typeId) {
const btn = $('step2')
const btnB = $('step2b')
btn.disabled = btnB.disabled = true
setTag('s2tag', 'busy', '获取中…')
setNum('s2num', false)
$('s2info').style.display = 'none'
try {
let url = base + '/ad/get' + (typeId ? '?type_ids=' + encodeURIComponent(typeId) : '')
const res = await fetch(nocache(url))
const data = await res.json()
$('s2pre').textContent = JSON.stringify(data, null, 2)
if (data.code === 200 && data.extra && data.extra.reqId) {
setTag('s2tag', 'ok', '已获取')
setNum('s2num', true)
$('s2info').style.display = 'grid'
const adKeys = data.ads ? Object.keys(data.ads) : []
let adId = '-', text = '-'
if (adKeys.length > 0) {
const ad = data.ads[adKeys[0]]
adId = ad.ad_id || '-'
text = ad.text || '-'
}
$('s2adId').textContent = adId
$('s2reqId').textContent = data.extra.reqId
$('s2text').textContent = text
$('step3').disabled = false
return data.extra.reqId
}
setTag('s2tag', 'no', '未获取到 reqId')
return null
} catch (e) {
$('s2pre').textContent = JSON.stringify({ error: e.message }, null, 2)
setTag('s2tag', 'no', e.message)
return null
} finally {
btn.disabled = false
btnB.disabled = false
}
}
$('step2').onclick = () => step2()
$('step2b').onclick = () => step2('["400002_0"]')
// ===== 步骤 3: 领取权益 =====
async function step3() {
const btn = $('step3')
btn.disabled = true
setTag('s3tag', 'busy', '领取中…')
setNum('s3num', false)
$('s3info').style.display = 'none'
try {
const res = await fetch(nocache(base + '/ad/listening/rights/gain'))
const data = await res.json()
$('s3pre').textContent = JSON.stringify(data, null, 2)
if (data.code === 200) {
setTag('s3tag', 'ok', '已领取')
setNum('s3num', true)
$('s3info').style.display = 'grid'
const body = data.data || {}
$('s3result').textContent = JSON.stringify(body).substring(0, 200)
// 领取成功后自动刷新剩余时长
queryRights()
return true
}
setTag('s3tag', 'no', '领取失败')
return false
} catch (e) {
$('s3pre').textContent = JSON.stringify({ error: e.message }, null, 2)
setTag('s3tag', 'no', e.message)
return false
} finally {
btn.disabled = false
}
}
$('step3').onclick = step3
// ===== 一键运行 =====
$('runAll').onclick = async function () {
const btn = this
btn.disabled = true
setTag('allTag', 'busy', '运行中…')
await step1()
await step2()
await step3()
setTag('allTag', 'ok', '完成')
btn.disabled = false
}
// ===== 清空 =====
$('clearAll').onclick = function () {
const steps = ['s1', 's2', 's3']
const tags = { s1: '未开始', s2: '未开始', s3: '未开始' }
steps.forEach(s => {
setTag(s + 'tag', 'no', tags[s])
setNum(s + 'num', false)
$(s + 'pre').textContent = '(尚未执行)'
const info = $(s + 'info')
if (info) info.style.display = 'none'
})
$('step1').disabled = false
$('step2').disabled = true
$('step2b').disabled = true
$('step3').disabled = true
setTag('allTag', 'no', '就绪')
$('runAll').disabled = false
// 重置计时器
stopCountdown()
$('timerDays').textContent = '--'
$('timerHours').textContent = '--'
$('timerMinutes').textContent = '--'
$('timerSeconds').textContent = '--'
setTag('rightsTag', 'no', '未查询')
$('rightsInfo').style.display = 'none'
$('rightsTitleDisplay').textContent = '🕐 免费听时长剩余'
$('rightsSubDesc').textContent = '查询你的免费听权益剩余时长'
$('runAll').disabled = true
}
// ===== 剩余时长计时器 =====
let countdownTimer = null
let remainingMs = 0
/** 毫秒 → { d, h, m, s } */
function parseMs(ms) {
if (ms <= 0) return { d: 0, h: 0, m: 0, s: 0 }
const totalSec = Math.floor(ms / 1000)
return {
d: Math.floor(totalSec / 86400),
h: Math.floor((totalSec % 86400) / 3600),
m: Math.floor((totalSec % 3600) / 60),
s: totalSec % 60
}
}
function renderTimer(ms) {
const t = parseMs(ms)
$('timerDays').textContent = String(t.d).padStart(2, '0')
$('timerHours').textContent = String(t.h).padStart(2, '0')
$('timerMinutes').textContent = String(t.m).padStart(2, '0')
$('timerSeconds').textContent = String(t.s).padStart(2, '0')
}
function stopCountdown() {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
}
function startCountdown(ms) {
stopCountdown()
remainingMs = Math.max(0, ms)
renderTimer(remainingMs)
if (remainingMs <= 0) {
setTag('rightsTag', 'no', '已用完')
return
}
countdownTimer = setInterval(() => {
remainingMs = Math.max(0, remainingMs - 1000)
renderTimer(remainingMs)
if (remainingMs <= 0) {
stopCountdown()
setTag('rightsTag', 'no', '已过期')
}
}, 1000)
}
async function queryRights() {
const btn = $('queryRightsBtn')
btn.disabled = true
setTag('rightsTag', 'busy', '查询中…')
try {
const res = await fetch(nocache(base + '/ad/listening/rights'))
const data = await res.json()
if (data.code === 200 && data.data) {
const d = data.data
$('rightsTitleDisplay').textContent = '🕐 ' + (d.title || '免费听时长剩余')
$('rightsSubDesc').textContent = d.vipInfoContent || '查询你的免费听权益剩余时长'
$('rightsInfo').style.display = 'grid'
// 状态
const status = d.status || ''
$('rightsStatus').textContent = status
// 结束时间
if (d.rightsEndTime) {
const endDate = new Date(d.rightsEndTime)
$('rightsEndTimeDisplay').textContent = endDate.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
}
// 今日已覆盖
$('rightsCoverToday').textContent = d.rightsCoverToday ? '是 ✓' : '否'
// 倒计时
if (d.rightsRemainingTime > 0) {
startCountdown(d.rightsRemainingTime)
setTag('rightsTag', 'ok', '已解锁 ✓')
} else {
renderTimer(0)
setTag('rightsTag', 'no', '已用完')
}
// 领取按钮(仅当 step3 可用时启用)
if (d.cardContent && d.cardContent.actionTitle && $('step3').disabled === false) {
$('claim30Btn').disabled = false
$('claim30Btn').textContent = d.cardContent.actionTitle
} else {
$('claim30Btn').disabled = true
}
if (d.rightsUpperLimit) {
$('rightsStatus').textContent += ' (已达上限)'
}
return data.data
}
setTag('rightsTag', 'no', '查询失败')
return null
} catch (e) {
setTag('rightsTag', 'no', e.message)
return null
} finally {
btn.disabled = false
}
}
$('queryRightsBtn').onclick = queryRights
$('claim30Btn').onclick = async function () {
const ok = await step3()
if (ok) await queryRights()
}
// 页面加载后自动查询一次
document.addEventListener('DOMContentLoaded', () => {
setTimeout(queryRights, 300)
})
</script>
</body>
</html>
+7 -1
View File
@@ -95,7 +95,13 @@ curl -s {origin}/search?keywords=网易云</code></pre>
<a href="/api_decrypt.html">API 解密</a> · <a href="/api_decrypt.html">API 解密</a> ·
<a href="/listen_together_host.html">一起听示例</a> · <a href="/listen_together_host.html">一起听示例</a> ·
<a href="/playlist_cover_update.html">更新歌单封面示例</a> · <a href="/playlist_cover_update.html">更新歌单封面示例</a> ·
<a href="/avatar_update.html">头像更新示例</a> <a href="/avatar_update.html">头像更新示例</a> ·
<a href="/scrobble.html">听歌打卡示例</a> ·
<a href="/yidun.html">获取 CheckToken</a> ·
<a href="/free_listen.html">免费听权益</a> ·
<a href="/yunbei_task.html">云贝广告任务</a> ·
<a href="/ugc.html">云小编任务中心</a> ·
<a href="/ugc_lottery.html">云小编每日抽奖</a>
</div> </div>
</section> </section>
+3 -3
View File
@@ -148,12 +148,12 @@
updateStatus('二维码已过期,请刷新页面', 'error') updateStatus('二维码已过期,请刷新页面', 'error')
clearInterval(timer) clearInterval(timer)
} else if (statusRes.code === 801) { } else if (statusRes.code === 801) {
updateStatus('二维码已扫描,请在手机上确认', 'waiting') updateStatus('等待手机扫码...', 'waiting')
} else if (statusRes.code === 802) { } else if (statusRes.code === 802) {
updateStatus('登录成功,正在保存信息...', 'waiting') updateStatus('二维码已扫描,请在手机上确认', 'waiting')
} else if (statusRes.code === 803) { } else if (statusRes.code === 803) {
clearInterval(timer) clearInterval(timer)
updateStatus('授权登录成功!', 'success') updateStatus('授权登录成功,正在保存信息...', 'success')
await getLoginStatus(statusRes.cookie) await getLoginStatus(statusRes.cookie)
localStorage.setItem('cookie', statusRes.cookie) localStorage.setItem('cookie', statusRes.cookie)
} }
+3 -3
View File
@@ -148,12 +148,12 @@
updateStatus('二维码已过期,请刷新页面', 'error') updateStatus('二维码已过期,请刷新页面', 'error')
clearInterval(timer) clearInterval(timer)
} else if (statusRes.code === 801) { } else if (statusRes.code === 801) {
updateStatus('二维码已扫描,请在手机上确认', 'waiting') updateStatus('等待手机扫码...', 'waiting')
} else if (statusRes.code === 802) { } else if (statusRes.code === 802) {
updateStatus('登录成功,正在保存信息...', 'waiting') updateStatus('二维码已扫描,请在手机上确认', 'waiting')
} else if (statusRes.code === 803) { } else if (statusRes.code === 803) {
clearInterval(timer) clearInterval(timer)
updateStatus('授权登录成功!', 'success') updateStatus('授权登录成功,正在保存信息...', 'success')
await getLoginStatus(statusRes.cookie) await getLoginStatus(statusRes.cookie)
localStorage.setItem('cookie', statusRes.cookie) localStorage.setItem('cookie', statusRes.cookie)
} }
+480
View File
@@ -0,0 +1,480 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>听歌打卡 - 网易云音乐 API Enhanced</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
min-height: 100vh;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 520px;
margin: 40px auto;
background: white;
border-radius: 12px;
padding: 32px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
text-align: center;
}
.subtitle {
font-size: 14px;
color: #666;
margin-bottom: 24px;
text-align: center;
}
.login-link {
display: block;
margin-bottom: 20px;
color: #666;
font-size: 14px;
text-decoration: none;
text-align: center;
}
.login-link:hover {
color: #333;
text-decoration: underline;
}
/* ---- Tabs ---- */
.tabs {
display: flex;
gap: 0;
margin-bottom: 24px;
border-bottom: 2px solid #eee;
}
.tab-btn {
flex: 1;
padding: 10px 0;
background: none;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
font-size: 14px;
font-weight: 500;
color: #999;
cursor: pointer;
transition: color 0.2s, border-color 0.2s;
}
.tab-btn:hover {
color: #555;
}
.tab-btn.active {
color: #333;
border-bottom-color: #333;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* ---- Form ---- */
.form-group {
margin-bottom: 16px;
text-align: left;
}
label {
display: block;
font-size: 13px;
font-weight: 500;
color: #555;
margin-bottom: 6px;
}
label .tag {
display: inline-block;
font-size: 11px;
font-weight: 400;
padding: 1px 6px;
border-radius: 3px;
margin-left: 6px;
}
label .tag.r {
background: #fee2e2;
color: #991b1b;
}
label .tag.o {
background: #fef3c7;
color: #92400e;
}
input[type="text"],
input[type="number"],
textarea {
width: 100%;
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
outline: none;
font-family: inherit;
transition: border-color 0.2s;
}
input[type="text"]:focus,
input[type="number"]:focus,
textarea:focus {
border-color: #333;
}
textarea {
resize: vertical;
}
.row {
display: flex;
gap: 12px;
}
.row .form-group {
flex: 1;
}
.quick-fill {
margin-top: 5px;
font-size: 12px;
color: #666;
}
.quick-fill span {
color: #0066cc;
cursor: pointer;
margin-right: 8px;
}
.quick-fill span:hover {
text-decoration: underline;
}
.btn {
display: block;
width: 100%;
padding: 12px;
background: #333;
color: white;
font-size: 15px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
border: none;
text-align: center;
margin-top: 20px;
}
.btn:hover {
background: #555;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.result {
margin-top: 16px;
padding: 12px 16px;
border-radius: 6px;
font-size: 14px;
text-align: left;
white-space: pre-wrap;
word-break: break-all;
font-family: monospace;
}
.result.success {
background: #d1fae5;
color: #065f46;
}
.result.error {
background: #fee2e2;
color: #991b1b;
}
.result.info {
background: #e0f2fe;
color: #0369a1;
}
.footer-link {
text-align: center;
margin-top: 24px;
font-size: 13px;
}
.footer-link a {
color: #666;
text-decoration: none;
}
.footer-link a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>听歌打卡</h1>
<p class="subtitle">同步听歌记录至网易云音乐,增加听歌排行计数</p>
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
<!-- Cookie (共享) -->
<div class="form-group">
<label for="cookie">Cookie <span class="tag o">可选</span></label>
<textarea id="cookie" placeholder="留空则默认读取本地存储的登录态" rows="2"></textarea>
<div class="quick-fill">
<span onclick="loadLocalCookie()">读取本地 Cookie</span>
</div>
</div>
<!-- Tabs -->
<div class="tabs">
<button class="tab-btn active" data-tab="v0" onclick="switchTab('v0')">原版</button>
<button class="tab-btn" data-tab="v1" onclick="switchTab('v1')">V2 (NCBL 加密版)</button>
</div>
<!-- ============ Tab: v0 (原版 /scrobble) ============ -->
<div class="tab-content active" id="tab-v0">
<div class="form-group">
<label>歌曲 ID <span class="tag r">必填</span></label>
<input type="text" id="v0-songId" placeholder="例如: 2756058128" value="2756058128" />
<div class="quick-fill">
示例:
<span onclick="fillV0('2756058128', '288651229')">妖精小姐的魔法邀约</span>
<span onclick="fillV0('2637402867', '251025018')">Echoes of Memoria</span>
<span onclick="fillV0('36307815', '3394198')">Lose Control</span>
</div>
</div>
<div class="form-group">
<label>来源 ID (歌单或专辑 ID) <span class="tag r">必填</span></label>
<input type="text" id="v0-sourceId" placeholder="例如: 2756058128" value="2756058128" />
</div>
<div class="form-group">
<label>播放时间 (秒) <span class="tag o">可选</span></label>
<input type="number" id="v0-time" placeholder="300" value="300" />
</div>
<button class="btn" onclick="submitV0()">立即打卡 (原版)</button>
</div>
<!-- ============ Tab: v1 (NCBL 加密版 /scrobble/v1) ============ -->
<div class="tab-content" id="tab-v1">
<div class="form-group">
<label>歌曲 ID <span class="tag r">必填</span></label>
<input type="text" id="v1-songId" placeholder="例如: 518066366" value="518066366" />
<div class="quick-fill">
示例:
<span onclick="fillV1('518066366', '36780169', '291')">默认示例</span>
<span onclick="fillV1('2756058128', '288651229', '300')">妖精小姐的魔法邀约</span>
<span onclick="fillV1('2637402867', '251025018', '300')">Echoes of Memoria</span>
</div>
</div>
<div class="form-group">
<label>播放时间 (秒) <span class="tag r">必填</span></label>
<input type="number" id="v1-time" placeholder="例如: 291" value="291" />
</div>
<div class="form-group">
<label>来源 ID <span class="tag o">可选</span></label>
<input type="text" id="v1-sourceId" placeholder="歌单或专辑 ID" value="36780169" />
</div>
<div class="row">
<div class="form-group">
<label>歌曲名 <span class="tag o">可选</span></label>
<input type="text" id="v1-name" placeholder="歌曲名称" />
</div>
<div class="form-group">
<label>艺术家 <span class="tag o">可选</span></label>
<input type="text" id="v1-artist" placeholder="歌手名" />
</div>
</div>
<div class="row">
<div class="form-group">
<label>码率 <span class="tag o">可选</span></label>
<input type="number" id="v1-bitrate" placeholder="默认 320" value="320" />
</div>
<div class="form-group">
<label>音质等级 <span class="tag o">可选</span></label>
<input type="text" id="v1-level" placeholder="默认 exhigh" value="exhigh" />
</div>
</div>
<div class="form-group">
<label>歌曲总时长 (秒) <span class="tag o">可选</span></label>
<input type="number" id="v1-total" placeholder="留空则与播放时间相同" />
</div>
<button class="btn" onclick="submitV1()">立即打卡 (NCBL 加密版)</button>
</div>
<!-- Result -->
<div id="result" class="result" style="display: none;"></div>
<div class="footer-link">
<a href="/">返回首页</a>
</div>
</div>
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
<script>
/* ---- DOM refs ---- */
const cookieInput = document.getElementById('cookie')
const resultDiv = document.getElementById('result')
/* ---- init ---- */
if (localStorage.getItem('cookie')) {
cookieInput.value = localStorage.getItem('cookie')
}
/* ---- Tab switching ---- */
function switchTab(name) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === name))
document.querySelectorAll('.tab-content').forEach(c => c.classList.toggle('active', c.id === 'tab-' + name))
hideResult()
}
/* ---- Cookie ---- */
function loadLocalCookie() {
const local = localStorage.getItem('cookie')
if (local) {
cookieInput.value = local
showResult('已成功读取本地 Cookie', 'success')
} else {
showResult('未在本地发现登录 Cookie,请先登录或手动粘贴', 'error')
}
}
/* ---- Quick fill ---- */
function fillV0(id, sourceId) {
document.getElementById('v0-songId').value = id
document.getElementById('v0-sourceId').value = sourceId
}
function fillV1(id, sourceId, time) {
document.getElementById('v1-songId').value = id
document.getElementById('v1-sourceId').value = sourceId
document.getElementById('v1-time').value = time
}
/* ---- Result ---- */
function hideResult() {
resultDiv.style.display = 'none'
}
function showResult(message, type) {
resultDiv.textContent = message
resultDiv.className = 'result ' + type
resultDiv.style.display = 'block'
}
function disableAll(disabled) {
document.querySelectorAll('.btn').forEach(b => b.disabled = disabled)
}
/* ---- Submit: 原版 /scrobble ---- */
async function submitV0() {
const id = document.getElementById('v0-songId').value.trim()
const sourceid = document.getElementById('v0-sourceId').value.trim()
const time = document.getElementById('v0-time').value.trim() || '300'
if (!id) { showResult('请输入歌曲 ID !', 'error'); return }
if (!sourceid) { showResult('请输入来源 ID !', 'error'); return }
disableAll(true)
showResult('打卡中,请稍候...', 'info')
try {
const params = { id, sourceid: sourceid || id, time, timestamp: Date.now() }
const cookie = cookieInput.value.trim()
if (cookie) params.cookie = cookie
const res = await axios({ method: 'get', url: '/scrobble', params })
if (res.data.code === 200) {
showResult('✅ 打卡成功! \n\n' + JSON.stringify(res.data, null, 2), 'success')
} else {
showResult('请求已发送,返回:\n\n' + JSON.stringify(res.data, null, 2), 'info')
}
} catch (error) {
const errData = error.response ? error.response.data : { error: error.message }
showResult('❌ 打卡失败\n\n' + JSON.stringify(errData, null, 2), 'error')
} finally {
disableAll(false)
}
}
/* ---- Submit: NCBL 加密版 /scrobble/v1 ---- */
async function submitV1() {
const id = document.getElementById('v1-songId').value.trim()
const time = document.getElementById('v1-time').value.trim()
if (!id) { showResult('请输入歌曲 ID !', 'error'); return }
if (!time || isNaN(+time) || +time <= 0) { showResult('请输入有效的播放时长(秒)!', 'error'); return }
disableAll(true)
showResult('打卡中(NCBL 加密),请稍候...', 'info')
try {
const params = {
id,
time,
sourceid: document.getElementById('v1-sourceId').value.trim() || undefined,
name: document.getElementById('v1-name').value.trim() || undefined,
artist: document.getElementById('v1-artist').value.trim() || undefined,
bitrate: document.getElementById('v1-bitrate').value.trim() || undefined,
level: document.getElementById('v1-level').value.trim() || undefined,
total: document.getElementById('v1-total').value.trim() || undefined,
timestamp: Date.now(),
}
const cookie = cookieInput.value.trim()
if (cookie) params.cookie = cookie
// 清理 undefined 参数
Object.keys(params).forEach(k => params[k] === undefined && delete params[k])
const res = await axios({ method: 'get', url: '/scrobble/v1', params })
if (res.data.code === 200) {
showResult('✅ NCBL 加密打卡成功! \n\n' + JSON.stringify(res.data, null, 2), 'success')
} else {
showResult('请求已发送,返回:\n\n' + JSON.stringify(res.data, null, 2), 'info')
}
} catch (error) {
const errData = error.response ? error.response.data : { error: error.message }
showResult('❌ 打卡失败\n\n' + JSON.stringify(errData, null, 2), 'error')
} finally {
disableAll(false)
}
}
</script>
</body>
</html>

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