Compare commits

...
225 Commits
Author SHA1 Message Date
MoeFurina a366983e99 feat: 新增评论举报接口 && bump 4.32.1 2026-05-16 10:21:46 +08:00
MoEFusion 15fa49a2e8 Merge pull request #164 from irtrdr/main 2026-05-01 14:00:51 +08:00
irtrdr f5fe69c863 feat: 更新song_copyright_rcmd接口参数为可选 2026-04-29 04:09:54 +08:00
irtrdr b6be789ccd feat: 新增灰色歌曲其他版本推荐接口 2026-04-29 03:55:21 +08:00
MoeFurina 0395d087d9 Merge branch 'main' into main 2026-04-19 10:19:41 +08:00
wlm3201 3f679c0d87 日推接口增加刷新参数 2026-04-18 11:40:13 +08:00
MoeFurina ba9c6deaee feat: 新增助眠解压相关模块 && bump 4.32.0 2026-04-17 20:41:31 +08:00
MoeFurina 2c36daf07a bump 4.31.0 2026-03-28 00:09:46 +08:00
ElyPrism 5b5f380cd5 Merge pull request #136 from laoshuikaixue/feature/cors-multi-domain 2026-03-20 23:39:06 +08:00
ElyPrism ff7c5518d1 Merge pull request #138 from NeteaseCloudMusicApiEnhanced/up-4.31.0 2026-03-20 20:14:14 +08:00
MoeFurina bbb2b9d4a0 docs: 更新文档 2026-03-20 20:08:47 +08:00
MoeFurina 3188238678 feat: 添加DIFM相关接口 2026-03-20 20:05:40 +08:00
MoeFurina 8e60324452 chore: update deps 2026-03-20 20:02:04 +08:00
MoeFurina c9909cddd3 Merge remote-tracking branch 'origin/dependabot/github_actions/actions/upload-artifact-7' into up-4.31.0 2026-03-20 19:58:27 +08:00
MoeFurina dbdbba3f98 Squashed commit of the following:
commit ed0208eb89
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Sun Mar 1 12:48:11 2026 +0000

    chore(deps): bump actions/download-artifact from 7 to 8

    Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
    - [Release notes](https://github.com/actions/download-artifact/releases)
    - [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

    ---
    updated-dependencies:
    - dependency-name: actions/download-artifact
      dependency-version: '8'
      dependency-type: direct:production
      update-type: version-update:semver-major
    ...

    Signed-off-by: dependabot[bot] <[email protected]>
2026-03-20 19:58:19 +08:00
LaoShui 27aa9a01cb fix(cors): 修复CORS源验证逻辑
- 移除无效的请求源回退逻辑
- 简化Vary头设置条件判断
- 优化CORS允许源验证流程
2026-03-14 21:14:18 +08:00
LaoShui 30e522018f feat(server): 支持多域名CORS配置
- 实现了逗号分隔的多域名CORS配置解析功能
- 新增parseCorsAllowOrigins函数处理域名列表解析
- 新增getCorsAllowOrigin函数实现动态域名匹配逻辑
- 更新CORS响应头设置逻辑支持多域名验证
- 添加Vary: Origin响应头优化缓存策略
- 修改README文档说明多域名配置方式
2026-03-14 21:03:58 +08:00
LaoShui 7b2507e43d refactor(server): 修复函数名拼写错误
- 将 consturctServer 函数名更正为 constructServer
- 更新调用处的函数名称以保持一致性
2026-03-14 19:24:52 +08:00
ElyPrism 3647fc5fa0 Merge branch 'add-new-modules' to main 2026-03-14 00:28:04 +08:00
MoeFurina 8521c198d3 docs: 更新readme 2026-03-14 00:25:43 +08:00
MoeFurina 4d8c4daa14 docs: 更新文档 2026-03-13 23:35:03 +08:00
ElyPrismandgemini-code-assist[bot] 2e3cf4d451 Apply suggestion from @gemini-code-assist[bot]
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-08 12:00:17 +08:00
ElyPrismandgemini-code-assist[bot] 288459cddb fix: 修复部分拼写错误
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-08 11:59:15 +08:00
ElyPrismandgemini-code-assist[bot] c1b8a381f3 fix: module/voicelist_search.js 拼写错误
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-08 11:53:03 +08:00
MoeFurina 2dd12b88ae feat: 新增评论相关接口并更新部分接口 2026-03-08 11:42:32 +08:00
ElyPrism b6a1f9ce0a Merge branch 'main' into main 2026-03-08 09:24:54 +08:00
RedSTAR.Cheng 5991e41c5d Merge branch 'NeteaseCloudMusicApiEnhanced:main' into main 2026-03-04 10:21:38 +08:00
dependabot[bot] f61554714c chore(deps): bump actions/upload-artifact from 6 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-01 12:48:05 +00:00
RedSTARO 266cd8d059 chore: doc update for 302 endpoint 2026-03-01 14:03:54 +08:00
RedSTARO 89a70b14d4 refactor: simplify /song/url/v1/302 call chain 2026-03-01 13:48:28 +08:00
ElyPrism a9332af453 Merge branch 'main' into main
build(version): bump to 4.30.2
2026-02-28 15:52:06 +08:00
MoeFurina cffe68d9ff upd changelog 2026-02-28 15:51:47 +08:00
MoeFurina 75ec2de66f feat: update doc icon 2026-02-28 15:37:02 +08:00
MoeFurina 0bd023c188 Merge branch 'main' into chore/bump-4.30.2 2026-02-28 15:33:16 +08:00
ElyPrism 2c82c9b7be Merge branch 'add-comment-info' into 'main' 2026-02-28 15:29:10 +08:00
imsyy 1320e5780b feat: 更新文档 2026-02-28 14:52:13 +08:00
imsyy bde1f0864b feat: 更新文档 2026-02-28 14:50:04 +08:00
imsyy e891287f1a feat: 添加评论统计数据 2026-02-28 14:42:25 +08:00
RedSTARO 59241631db feat: add song download url api end point for 302
* This will redirect access from http://localhost:3000/song/url/v1?id=2756058128&level=exhigh to actual resource url
2026-02-26 20:12:43 +08:00
MoeFurina 29ef4f68c2 docs: update readme 2026-02-26 18:43:17 +08:00
MoeFurina ee25eb17a6 docs: update readme 2026-02-23 23:30:55 +08:00
MoeFurina 31d2ec9225 chore: clean up empty code change sections in the changes log 2026-02-23 23:12:01 +08:00
ElyPrismandgemini-code-assist[bot] b45bdd411f Update README.MD
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-22 22:20:15 +08:00
MoeFurina c14b06f6c3 build: update deps 2026-02-22 22:16:39 +08:00
ElyPrism b027aca40a Merge branch 'feat/cloud-upload-modes' into 'main'
feat(cloud): 添加云盘上传模式选择功能并优化上传配置
2026-02-19 12:04:19 +08:00
LaoShui b1729f19aa style(code): 格式化代码
- 将长参数列表拆分为多行以提高可读性
- 标准化函数调用中的逗号使用
- 统一对象和数组的换行格式
- 更新 ESLint 配置中的 ECMA 版本至 2020,以支持 ES2020 语法(包括 Optional Chaining)
- 优化代码缩进和对齐方式
- 确保所有文件中的代码风格一致性
2026-02-19 11:52:19 +08:00
LaoShui 755489546b refactor(util): 简化文件帮助工具函数
- 移除未使用的 path 模块引入
- 将 getFileExtension 函数从异步改为同步
- 优化 sanitizeFilename 函数逻辑,移除冗余扩展名处理
2026-02-18 20:24:43 +08:00
LaoShui 92df6e13a0 refactor(cloud): 重构文件处理逻辑并提取辅助函数
- 将文件大小、MD5计算等逻辑提取到 fileHelper 工具模块
- 使用统一的文件扩展名和文件名处理函数
- 简化临时文件处理和清理逻辑
- 统一文件上传数据获取方式
- 移除重复的文件操作代码并提高可维护性
2026-02-18 20:19:46 +08:00
LaoShui 1ad8ab088f fix(cloud): 修改上传接口Cookie参数传递方式
- 将Cookie参数从URL查询字符串改为请求体传递
- 更新token接口调用以将cookie作为data参数发送
- 更新complete接口调用以将cookie作为data参数发送
- 修改API文档说明Cookie参数传递方式变更
- 确保Cookie值通过POST请求体安全传输
2026-02-18 19:37:32 +08:00
LaoShui a09f126ab2 fix(upload): 修复文件上传功能中的路径替换和临时文件支持
- 将单个斜杠替换改为全局正则替换以正确处理所有路径分隔符
- 移除未使用的 crypto 模块引入
- 添加对临时文件上传的支持,使用 fs 模块读取临时文件
- 动态设置 Content-Type 头部,优先使用文件的 mimetype 属性
- 重构多处文件上传逻辑以支持流式读取大文件
- 优化分片上传时的文件大小获取方式
2026-02-18 19:09:15 +08:00
LaoShui 872bae1b43 feat(cloud): 添加音频元数据解析功能
- 集成 jsmediatags 库用于解析音频文件的 ID3 标签
- 实现 parseMediaTags 函数提取歌曲标题、艺术家和专辑信息
- 在上传前添加音频元数据解析步骤
- 将解析的元数据传递给 completeUpload 函数
- 使用元数据中的标题作为歌曲名称,提高准确性
- 添加默认值处理以防止解析失败
- 更新 completeUpload 函数接收文件对象和媒体标签参数
- 移除模块中不再使用的文件扩展名提取逻辑
2026-02-18 18:49:05 +08:00
LaoShui 83c527af01 feat(cloud): 添加云盘上传模式选择和进度显示功能
- 添加客户端直传和后端代理两种上传模式选项
- 实现上传进度条和状态显示界面
- 添加文件MD5计算和上传凭证获取功能
- 支持大文件上传和断点续传机制
- 新增cloud/upload/token和cloud/upload/complete接口
- 更新文档说明上传模式和接口使用方法
- 优化上传按钮禁用状态和提示信息显示
2026-02-18 18:42:28 +08:00
LaoShui 84efe5d758 chore(deps): 更新依赖包版本并移除冗余依赖
- 移除了 express-fileupload 中的 md5 依赖
- 将多个开发依赖包从固定版本改为范围版本(^)
- 更新了 typescript、eslint 等核心依赖的版本声明
- 移除了 charenc、crypt、is-buffer 等冗余依赖包
- 更新了 @typescript-eslint 相关包的版本引用
- 清理了依赖树中的重复和无用条目
2026-02-18 17:24:45 +08:00
LaoShui cfa475bf94 chore(deps): 更新依赖包版本并移除不需要的依赖
- 移除了 md5 依赖包
- 移除了 @neteasecloudmusicapienhanced/unblockmusic-utils 重复依赖
- 移除了 axios 重复依赖
- 移除了 dotenv 重复依赖
- 移除了 music-metadata 重复依赖
- 将开发依赖包版本从固定版本号更新为 ^ 版本范围
- 统一了依赖包版本管理格式
2026-02-18 17:13:50 +08:00
LaoShui 4087bafd7d fix(cloud): 解决临时文件清理和上传流程中的错误处理问题
- 修复了临时文件清理失败时的日志消息本地化
- 移除了重复的临时文件清理调用,统一在 finally 块中处理
- 优化了错误处理逻辑,确保在各种异常情况下正确返回错误响应
- 更新了日志消息为中文描述,提高可读性
- 修复了上传插件中的日志消息本地化问题
- 统一了错误处理流程,避免重复的清理操作
2026-02-18 17:09:02 +08:00
LaoShui 8951e32a0e refactor(server): 优化文件上传配置和MD5计算逻辑
- 移除cloud.js中的异步Promise包装,直接同步计算MD5哈希值
- 在server.js中提取上传大小限制为常量配置
- 统一使用字节单位常量管理文件上传大小限制
- 简化代码结构,提高可读性和维护性
2026-02-18 17:02:15 +08:00
LaoShui 33ccc83615 fix(cloud): 解决临时文件清理和异步操作问题
- 添加异步临时文件清理函数避免资源泄露
- 将同步文件操作改为异步操作提高性能
- 在令牌分配失败时执行临时文件清理
- 在上传失败时确保临时文件被清理
- 使用 Promise 包装 MD5 计算操作
- 统一临时文件清理逻辑到 finally 块
2026-02-18 16:57:20 +08:00
LaoShui 2d6173b2aa fix(cloud): 移除了重复的条件判断 2026-02-18 16:50:20 +08:00
LaoShui ba7d1a8574 feat(cloud): 支持临时文件上传功能
- 使用 crypto 模块替换 md5 模块进行文件哈希计算
- 添加对临时文件上传的支持,当存在 tempFilePath 时使用文件流处理
- 实现临时文件的 MD5 计算和元数据解析功能
- 在上传完成后自动清理临时文件
- 配置服务器端文件上传中间件启用临时文件支持
- 修改上传插件以支持临时文件读取流上传方式
- 增加文件大小获取和验证的兼容性处理
2026-02-18 16:44:19 +08:00
LaoShui 26d55255e0 fix(cloud): 解决云上传功能中的错误处理和代码清理
- 移除注释掉的废弃代码和调试信息
- 添加详细的错误处理和日志记录
- 验证token分配结果并处理失败情况
- 在上传过程中添加异常捕获和错误抛出
- 验证LBS响应的有效性并处理网络请求超时
- 改进上传流程的错误处理机制
2026-02-18 16:17:07 +08:00
LaoShui 4489f10f63 feat(server): 增加文件上传大小限制并优化配置
- 将请求体大小限制从 50MB 增加到 500MB
- 配置文件上传中间件支持 500MB 大小限制
- 设置临时文件目录为 /tmp/
- 启用 useTempFiles 选项以提高性能
- 配置达到限制时自动中止上传
2026-02-18 16:08:40 +08:00
ElyPrism 2047bdfa65 Merge branch 'main' into 'main'
refactor: Refactor random Chinese IP generator
2026-02-15 00:20:25 +08:00
MoeFurina bc9d7b949f fix: 修复注入随机ip不生效 2026-02-15 00:06:42 +08:00
MoeFurina f08f192328 refactor: 重构随机中国ip 2026-02-14 23:50:59 +08:00
ElyPrism b04b9d51fb Merge branch 'main' of 'main'
更加优雅的显示URL和调试按钮
2026-02-14 12:09:45 +08:00
BakaCookie520 c91f5f3714 更加优雅的显示URL和调试按钮 2026-02-14 00:01:19 +08:00
ElyPrism 7fceec12e4 Merge branch "Burial0268/main" into "main" 2026-02-12 19:28:43 +08:00
拜瑞andgemini-code-assist[bot] 1df147edab fix(server): prevent obj[2]=undefined if there is no options passed
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-12 19:21:02 +08:00
Burial0268 6d1f75f229 fix(server): Correctly inject client IP into request parameters 2026-02-12 18:59:32 +08:00
2a0b54e442 feat: Add user playlist endpoints & domain overrides (bump 4.30.1) (#105)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-11 16:49:11 +08:00
github-actions[bot]andgithub-actions[bot] a19418bbb6 Auto-build: Add compiled binaries for win, linux, macos (#104)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-10 11:38:47 +08:00
ce51da9150 build(version): bump 4.30.0 (#102)
* chore(deps): bump actions/checkout from 4 to 6

Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

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

Signed-off-by: dependabot[bot] <[email protected]>

* Squashed commit of the following:

commit f73cbbeec3
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Sun Feb 1 12:47:57 2026 +0000

    chore(deps): bump actions/download-artifact from 4 to 7

    Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 7.
    - [Release notes](https://github.com/actions/download-artifact/releases)
    - [Commits](https://github.com/actions/download-artifact/compare/v4...v7)

    ---
    updated-dependencies:
    - dependency-name: actions/download-artifact
      dependency-version: '7'
      dependency-type: direct:production
      update-type: version-update:semver-major
    ...

    Signed-off-by: dependabot[bot] <[email protected]>

* Squashed commit of the following:

commit 4da2d5f360
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Sun Feb 1 12:48:00 2026 +0000

    chore(deps): bump actions/github-script from 7 to 8

    Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8.
    - [Release notes](https://github.com/actions/github-script/releases)
    - [Commits](https://github.com/actions/github-script/compare/v7...v8)

    ---
    updated-dependencies:
    - dependency-name: actions/github-script
      dependency-version: '8'
      dependency-type: direct:production
      update-type: version-update:semver-major
    ...

    Signed-off-by: dependabot[bot] <[email protected]>

* Squashed commit of the following:

commit 9e65b17625
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Sun Feb 1 12:47:49 2026 +0000

    chore(deps): bump actions/setup-node from 4 to 6

    Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
    - [Release notes](https://github.com/actions/setup-node/releases)
    - [Commits](https://github.com/actions/setup-node/compare/v4...v6)

    ---
    updated-dependencies:
    - dependency-name: actions/setup-node
      dependency-version: '6'
      dependency-type: direct:production
      update-type: version-update:semver-major
    ...

    Signed-off-by: dependabot[bot] <[email protected]>

* Squashed commit of the following:

commit a060f3c4cc
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Sun Feb 1 12:47:53 2026 +0000

    chore(deps): bump actions/upload-artifact from 4 to 6

    Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 6.
    - [Release notes](https://github.com/actions/upload-artifact/releases)
    - [Commits](https://github.com/actions/upload-artifact/compare/v4...v6)

    ---
    updated-dependencies:
    - dependency-name: actions/upload-artifact
      dependency-version: '6'
      dependency-type: direct:production
      update-type: version-update:semver-major
    ...

    Signed-off-by: dependabot[bot] <[email protected]>

* build(version): bump 4.30.0

* refactor: QR login and voice upload pages with improved UI and error handling

- Enhanced styling for better user experience on qrlogin-nocookie.html and qrlogin.html
- Added loading indicators and improved status messages during QR code login process
- Updated error handling for login status retrieval
- Refactored unblock_test.html for better layout and user interaction
- Improved voice upload page with a more intuitive design and better feedback for file uploads
- Added loading state management for voice list retrieval
- Enhanced accessibility and usability across all modified pages

* fix: Potential fix for code scanning alert no. 6: DOM text reinterpreted as HTML

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Potential fix for code scanning alert no. 15: DOM text reinterpreted as HTML

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* build: update utils

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-02-09 23:33:26 +08:00
Yon8 27002d7665 feat: add musician_vip_tasks API, its module, and home.md documentation. (#95) 2026-02-05 00:29:02 +08:00
github-actions[bot]andgithub-actions[bot] 43d1c72f8f Auto-build: Add compiled binaries for win, linux, macos (#88)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-31 14:37:48 +08:00
MoeFurina 1d7e9cb6d8 fix(workflow): 修复低级错误 2026-01-31 14:32:42 +08:00
ElyPrism 609c910d5a ci: 支持action自动编译可执行文件 2026-01-31 14:22:00 +08:00
MoeFurina 2b69e9dda5 chore(packages): bump version 4.29.21 2026-01-30 23:13:13 +08:00
MoeFurina 796c2803df Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/typescript-eslint/parser-8.53.0' 2026-01-30 23:08:07 +08:00
MoeFurina ab00919a93 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/types/node-25.0.9' 2026-01-30 23:07:58 +08:00
dependabot[bot] db2ca44bbc chore(packages): bump @typescript-eslint/parser from 8.51.0 to 8.53.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.51.0 to 8.53.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.53.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.53.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-01-19 14:35:05 +00:00
dependabot[bot] 6c79f7495b chore(packages): bump @types/node from 25.0.3 to 25.0.9
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.0.3 to 25.0.9.
- [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.0.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-01-19 14:34:32 +00:00
dependabot[bot] fd0608adef chore(packages): bump eslint-plugin-prettier from 5.5.4 to 5.5.5
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.5.4 to 5.5.5.
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.5.4...v5.5.5)

---
updated-dependencies:
- dependency-name: eslint-plugin-prettier
  dependency-version: 5.5.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-01-19 14:34:19 +00:00
f6fc452666 更新 README 以反映 Node.js 版本要求和 Docker 使用说明 (#80)
Co-authored-by: binaryify <[email protected]>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-16 22:58:19 +08:00
MoeFurina 748d5da8fe fix(require): fix modules not defined 2026-01-09 21:44:03 +08:00
dependabot[bot] 6613fbf50d chore(packages): bump @types/node from 24.10.0 to 24.10.1 (#57)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-03 01:50:08 +08:00
dependabot[bot] c8493b93ad chore(packages): bump @typescript-eslint/parser from 8.46.3 to 8.48.0 (#67)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-03 01:49:49 +08:00
dependabot[bot] b93578a879 chore(packages): bump prettier from 3.6.2 to 3.7.3 (#66)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-03 01:47:59 +08:00
dependabot[bot] de54c7fe97 chore(packages): bump @typescript-eslint/eslint-plugin (#59)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-03 01:47:38 +08:00
MoeFurina 2e707e2f5e Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/lint-staged-16.2.7' 2026-01-03 01:43:23 +08:00
MoeFurina 2994e2fadd Merge remote-tracking branch 'origin/dependabot/github_actions/peter-evans/create-or-update-comment-5' 2026-01-03 01:43:15 +08:00
MoeFurina eb08ed4ba3 Merge remote-tracking branch 'origin/dependabot/github_actions/actions/stale-10' 2026-01-03 01:43:09 +08:00
MoeFurina d5fedb5c54 Merge remote-tracking branch 'origin/dependabot/github_actions/actions/checkout-6' 2026-01-03 01:43:02 +08:00
MoeFurina f2f78b2774 fix(workflows): update branch references and remove unused push triggers
feat: enhance eapi_decrypt.html with styled decode result display

fix: update qrlogin and qrlogin-nocookie to include platform parameter in QR code creation URL

chore: bump music-metadata and other dependencies in package.json and pnpm-lock.yaml

docs: update README with important project usage notes
2026-01-03 01:36:21 +08:00
dependabot[bot] 871414f2e6 chore(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <[email protected]>
2025-12-26 11:55:00 +00:00
MoeFurina 2d3f15f0e5 revert: revive back workflow 2025-12-26 19:54:03 +08:00
ElyPrismandgemini-code-assist[bot] 598dd8bd34 refactor(unblock): Refactor song URL matching to use new utils package (#73)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-12-26 19:51:13 +08:00
ElyPrismandYiQing-House 30bd839809 feat(module): add vip daily signin (#72)
Co-authored-by: YiQing-House <[email protected]>
2025-12-20 10:14:50 +08:00
MoeFurina d4ee3b88f8 docs: upd doc 2025-12-20 00:45:53 +08:00
MoeFurina 77eb154028 fix(eslint): replace double quotes with single quotes 2025-12-19 21:05:04 +08:00
ElyPrism 140ea858c4 Merge pull request #70 from apoint123/fix/phone-login 2025-12-19 20:59:24 +08:00
apoint123 f7f8c54236 fix: 修复验证码登录 2025-12-18 18:21:05 +08:00
MoeFurina bcda215a6c chore(deps): update dependences 2025-12-12 20:24:24 +08:00
MoeFurina ca6834b83f ci(docker): fix cannot be deployed by linux arm64 2025-12-06 23:00:49 +08:00
dependabot[bot] c8fba0f7d1 chore(deps): bump actions/stale from 8 to 10
Bumps [actions/stale](https://github.com/actions/stale) from 8 to 10.
- [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/v8...v10)

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

Signed-off-by: dependabot[bot] <[email protected]>
2025-12-01 15:08:15 +00:00
dependabot[bot] 546a84b1b8 chore(deps): bump peter-evans/create-or-update-comment from 4 to 5
Bumps [peter-evans/create-or-update-comment](https://github.com/peter-evans/create-or-update-comment) from 4 to 5.
- [Release notes](https://github.com/peter-evans/create-or-update-comment/releases)
- [Commits](https://github.com/peter-evans/create-or-update-comment/compare/v4...v5)

---
updated-dependencies:
- dependency-name: peter-evans/create-or-update-comment
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-12-01 15:08:11 +00:00
ElyPrism 205ac7292f Merge pull request #62 from NeteaseCloudMusicApiEnhanced/chore/upd-deps 2025-11-29 12:36:11 +08:00
MoeFurina 1aba68561d 🐳 chore(deps): update dependences 2025-11-29 12:33:03 +08:00
ElyPrism bd2fdff5ae Merge pull request #55 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/music-metadata-11.10.3 2025-11-28 19:23:52 +08:00
dependabot[bot] 4af8323694 chore(packages): bump lint-staged from 16.2.6 to 16.2.7
Bumps [lint-staged](https://github.com/lint-staged/lint-staged) from 16.2.6 to 16.2.7.
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v16.2.6...v16.2.7)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-version: 16.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-24 14:15:19 +00:00
dependabot[bot] a5a1a91dfa chore(packages): bump music-metadata from 11.10.0 to 11.10.3
Bumps [music-metadata](https://github.com/Borewit/music-metadata) from 11.10.0 to 11.10.3.
- [Release notes](https://github.com/Borewit/music-metadata/releases)
- [Commits](https://github.com/Borewit/music-metadata/compare/v11.10.0...v11.10.3)

---
updated-dependencies:
- dependency-name: music-metadata
  dependency-version: 11.10.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-24 14:15:05 +00:00
ElyPrism ee43878834 Merge pull request #53 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/mocha-11.7.5 2025-11-15 15:25:31 +08:00
MoeFurina 9d18639464 fix(eslint): fix files which have eslint problems 2025-11-15 13:25:09 +08:00
MoeFurina fe28194445 refactor(generateRandomChineseIP): 完善随机中国ip功能 2025-11-15 13:11:29 +08:00
dependabot[bot] b14cb9bb24 chore(packages): bump mocha from 11.7.4 to 11.7.5
Bumps [mocha](https://github.com/mochajs/mocha) from 11.7.4 to 11.7.5.
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/v11.7.5/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v11.7.4...v11.7.5)

---
updated-dependencies:
- dependency-name: mocha
  dependency-version: 11.7.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-15 03:18:39 +00:00
ElyPrism 06739b2d6d Merge pull request #51 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/typescript-eslint/parser-8.46.3 2025-11-15 00:31:26 +08:00
MoeFurina 8639c9d0f1 chore(deps): update dependences 2025-11-15 00:28:14 +08:00
dependabot[bot] bc4a0d4916 chore(packages): bump @typescript-eslint/parser from 8.46.2 to 8.46.3
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.46.2 to 8.46.3.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.3/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.46.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-10 14:48:48 +00:00
MoeFurina 4e00b94ee6 chore: 删除过时的issue模板和更新bug报告及功能请求模板 2025-11-09 10:51:14 +08:00
MoeFurina 9019a2a4a5 fix(type): covert "music-metadata" esm to cjs 2025-11-09 08:21:51 +08:00
MoeFurina 3a10a4d753 ci(debug): add more debug info 2025-11-08 15:28:36 +08:00
MoeFurina f39aafaff4 docs: update doc 2025-11-08 12:52:19 +08:00
ElyPrism 2a8fb72d1d Merge pull request #41 from NeteaseCloudMusicApiEnhanced/dependabot/github_actions/actions/setup-node-6 2025-11-07 22:13:52 +08:00
ElyPrism ea6ab4f6f7 Merge pull request #42 from NeteaseCloudMusicApiEnhanced/dependabot/github_actions/docker/build-push-action-6 2025-11-07 22:13:42 +08:00
ElyPrism 6fe19a71e3 Merge pull request #43 from NeteaseCloudMusicApiEnhanced/dependabot/github_actions/actions/checkout-5 2025-11-07 22:13:30 +08:00
ElyPrism abd3a79ecd Merge branch 'main' into dependabot/github_actions/actions/checkout-5 2025-11-07 22:13:21 +08:00
ElyPrism 044ef355c1 Merge pull request #47 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/types/node-24.10.0 2025-11-07 22:13:07 +08:00
ElyPrism 2944ca42ba Merge pull request #45 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/music-metadata-11.9.0 2025-11-07 22:12:52 +08:00
MoeFurina d4d465e731 chore(deps): update dependences 2025-11-07 19:03:04 +08:00
ElyPrism f49d3cac19 Merge pull request #48 from Cinvin/main 2025-11-07 19:00:32 +08:00
5unV 2f2755ac8c feat: 增加云盘歌词接口 2025-11-06 10:04:13 +08:00
dependabot[bot] f46c0d4a84 chore(packages): bump @types/node from 24.9.1 to 24.10.0
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.9.1 to 24.10.0.
- [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: 24.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-03 12:42:34 +00:00
dependabot[bot] 14ee86e15b chore(packages): bump music-metadata from 7.14.0 to 11.9.0
Bumps [music-metadata](https://github.com/Borewit/music-metadata) from 7.14.0 to 11.9.0.
- [Release notes](https://github.com/Borewit/music-metadata/releases)
- [Commits](https://github.com/Borewit/music-metadata/compare/v7.14.0...v11.9.0)

---
updated-dependencies:
- dependency-name: music-metadata
  dependency-version: 11.9.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-03 12:42:01 +00:00
dependabot[bot] 5e820187a1 chore(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-01 12:18:20 +00:00
dependabot[bot] 781299c1c5 chore(deps): bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-01 12:18:15 +00:00
dependabot[bot] 8fc6ed1a76 chore(deps): bump actions/setup-node from 5 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-01 12:18:12 +00:00
MoeFurina 8bb68accee ci(action): fix workflows which pnpm is not installed 2025-11-01 13:23:30 +08:00
MoeFurina 55858d89d9 ci(action): switch GitHub Actions to use pnpm and update actions 2025-11-01 13:16:50 +08:00
MoeFurina 597ac1498f fix(eslint): fix files which have eslint problems 2025-11-01 13:11:38 +08:00
MoeFurina 398f410109 fix: update music search keyword and test case ID
- Changed the default search keyword in the API documentation from "这么可爱真是抱歉" to "妖精小姐的魔法邀约".
- Updated the test case for fetching song URL to use the correct song ID (from 1969519579 to 2756058128).
2025-11-01 12:16:29 +08:00
ElyPrism 97980b5ac3 Merge pull request #36 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/eslint-config-prettier-10.1.8
chore(packages): bump eslint-config-prettier from 8.5.0 to 10.1.8
2025-10-29 13:09:14 +08:00
ElyPrism 65ff21526a Merge pull request #37 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/types/mocha-10.0.10
chore(packages): bump @types/mocha from 9.1.1 to 10.0.10
2025-10-29 13:09:06 +08:00
ElyPrism fb052fdecd Merge pull request #38 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/typescript-5.9.3
chore(packages): bump typescript from 4.5.2 to 5.9.3
2025-10-29 13:08:58 +08:00
ElyPrism b6453a1c1b Merge pull request #39 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/yargs-18.0.0
chore(packages): bump yargs from 17.7.2 to 18.0.0
2025-10-29 13:08:49 +08:00
ElyPrism 0c3da3bcb7 Merge pull request #40 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/lint-staged-16.2.6
chore(packages): bump lint-staged from 16.2.4 to 16.2.6
2025-10-29 13:08:38 +08:00
dependabot[bot] 80fbd7a620 chore(packages): bump lint-staged from 16.2.4 to 16.2.6
Bumps [lint-staged](https://github.com/lint-staged/lint-staged) from 16.2.4 to 16.2.6.
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v16.2.4...v16.2.6)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-version: 16.2.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-27 13:51:16 +00:00
dependabot[bot] a4c9c0edaf chore(packages): bump yargs from 17.7.2 to 18.0.0
Bumps [yargs](https://github.com/yargs/yargs) from 17.7.2 to 18.0.0.
- [Release notes](https://github.com/yargs/yargs/releases)
- [Changelog](https://github.com/yargs/yargs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/yargs/yargs/compare/v17.7.2...v18.0.0)

---
updated-dependencies:
- dependency-name: yargs
  dependency-version: 18.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-27 13:50:05 +00:00
dependabot[bot] 19e51bc39a chore(packages): bump typescript from 4.5.2 to 5.9.3
Bumps [typescript](https://github.com/microsoft/TypeScript) from 4.5.2 to 5.9.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v4.5.2...v5.9.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 5.9.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-27 13:39:32 +00:00
dependabot[bot] a914b5bc11 chore(packages): bump @types/mocha from 9.1.1 to 10.0.10
Bumps [@types/mocha](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/mocha) from 9.1.1 to 10.0.10.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/mocha)

---
updated-dependencies:
- dependency-name: "@types/mocha"
  dependency-version: 10.0.10
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-27 13:39:31 +00:00
dependabot[bot] 5eeeaeee8a chore(packages): bump eslint-config-prettier from 8.5.0 to 10.1.8
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.5.0 to 10.1.8.
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.5.0...v10.1.8)

---
updated-dependencies:
- dependency-name: eslint-config-prettier
  dependency-version: 10.1.8
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-27 13:21:55 +00:00
ElyPrism 1ab9def4d5 Merge pull request #32 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/husky-9.1.7 2025-10-25 12:14:59 +08:00
dependabot[bot] 446ad4c871 chore(packages): bump husky from 7.0.4 to 9.1.7
Bumps [husky](https://github.com/typicode/husky) from 7.0.4 to 9.1.7.
- [Release notes](https://github.com/typicode/husky/releases)
- [Commits](https://github.com/typicode/husky/compare/v7.0.4...v9.1.7)

---
updated-dependencies:
- dependency-name: husky
  dependency-version: 9.1.7
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-25 04:14:44 +00:00
ElyPrism 316826dd81 Merge pull request #33 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/typescript-eslint/parser-5.62.0 2025-10-25 12:13:47 +08:00
ElyPrism 11a240cb18 Merge pull request #24 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/mocha-11.7.4 2025-10-25 12:13:39 +08:00
ElyPrism df2d0e6431 Merge pull request #34 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/types/node-24.8.1 2025-10-25 12:13:29 +08:00
ElyPrism a1436e1d64 Merge pull request #26 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/eslint-plugin-prettier-5.5.4 2025-10-25 12:13:19 +08:00
dependabot[bot] 1f87ccf080 chore(packages): bump @types/node from 24.6.1 to 24.8.1
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.6.1 to 24.8.1.
- [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: 24.8.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-25 04:12:03 +00:00
dependabot[bot] 323b1014d2 chore(packages): bump @typescript-eslint/parser from 5.0.0 to 5.62.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.0.0 to 5.62.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.62.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 5.62.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-25 04:12:00 +00:00
dependabot[bot] d2351f6f30 chore(packages): bump mocha from 11.7.3 to 11.7.4
Bumps [mocha](https://github.com/mochajs/mocha) from 11.7.3 to 11.7.4.
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v11.7.3...v11.7.4)

---
updated-dependencies:
- dependency-name: mocha
  dependency-version: 11.7.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-25 04:11:54 +00:00
dependabot[bot] dd653c07e9 chore(packages): bump eslint-plugin-prettier from 4.0.0 to 5.5.4
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 4.0.0 to 5.5.4.
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v4.0.0...v5.5.4)

---
updated-dependencies:
- dependency-name: eslint-plugin-prettier
  dependency-version: 5.5.4
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-25 04:11:52 +00:00
MoeFurina 173496f305 build(ci): delete useless lock files 2025-10-25 12:10:23 +08:00
MoeFurina b45739b621 refactor(index): rewrite api index pages without ai and update dependences 2025-10-24 17:51:23 +08:00
ElyPrism f63068b386 Merge pull request #31 from NeteaseCloudMusicApiEnhanced/fix/user_accout-returns-null 2025-10-18 14:21:16 +08:00
ElyPrism 927b79030b edit funding sources in FUNDING.yml
Removed WeChat & Alipay from funding options.

Signed-off-by: ElyPrism <[email protected]>
2025-10-18 14:20:38 +08:00
ImFurina 6d6eee13b9 docs: upd readme 2025-10-17 22:01:32 +08:00
ElyPrism a7f086cf8f Merge pull request #30 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/lint-staged-16.2.4 2025-10-17 21:39:31 +08:00
dependabot[bot] a38d689de4 chore(packages): bump lint-staged from 12.1.7 to 16.2.4
Bumps [lint-staged](https://github.com/lint-staged/lint-staged) from 12.1.7 to 16.2.4.
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v12.1.7...v16.2.4)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-version: 16.2.4
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-17 13:39:20 +00:00
ElyPrism 4ffb20c4ff Merge pull request #25 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-5.62.0 2025-10-17 21:37:34 +08:00
ElyPrism 84c19ee237 Merge pull request #22 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/dotenv-17.2.3 2025-10-17 21:37:22 +08:00
ImFurina f3d95f17b4 build(env): disable global dotenv file 2025-10-17 21:35:27 +08:00
ImFurina 6d33efee47 build(ci): change the package name 2025-10-17 20:06:30 +08:00
dependabot[bot] acc458f6cb chore(packages): bump @typescript-eslint/eslint-plugin
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.0.0 to 5.62.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.62.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 5.62.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-08 03:42:32 +00:00
ImFurina d78a20ea11 feat(randomCNIP): 添加随机中国IP选项 2025-10-08 11:40:40 +08:00
ImFurina 2cbd17a6d7 fix(typeerror): 尝试修复vercel调用 500错误 2025-10-07 15:08:05 +08:00
dependabot[bot] 240d8042fb chore(packages): bump dotenv from 16.6.1 to 17.2.3
Bumps [dotenv](https://github.com/motdotla/dotenv) from 16.6.1 to 17.2.3.
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v16.6.1...v17.2.3)

---
updated-dependencies:
- dependency-name: dotenv
  dependency-version: 17.2.3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-06 14:52:04 +00:00
ElyPrism f9f6db3fee Merge pull request #23 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/multi-c1a69033e5
chore(packages): bump express and @types/express
2025-10-06 22:41:56 +08:00
dependabot[bot] 89e7c98412 chore(packages): bump express and @types/express
Bumps [express](https://github.com/expressjs/express) and [@types/express](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/express). These dependencies needed to be updated together.

Updates `express` from 4.21.2 to 5.1.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.2...v5.1.0)

Updates `@types/express` from 4.17.23 to 5.0.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/express)

---
updated-dependencies:
- dependency-name: express
  dependency-version: 5.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
- dependency-name: "@types/express"
  dependency-version: 5.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-06 12:58:19 +00:00
ElyPrism 16bf878135 ci(workflow): fix the workflow file that package name is illegal 2025-10-06 20:54:58 +08:00
ImFurina 8dcbe4db9d ci: add ghcr imagine 2025-10-06 20:28:44 +08:00
ElyPrism a1fbcf7008 add fundings 2025-10-06 17:38:12 +08:00
ImFurina c9c881ba7d docs(index): 指向官方文档 2025-10-06 14:11:09 +08:00
ImFurina 0e0777d175 feat: 支持全局cookie代理 2025-10-06 14:07:33 +08:00
ImFurina 765734cd2d chore(dep): update dependence 2025-10-06 10:10:02 +08:00
ImFurina 7f3be34fdd 4.29.8 2025-10-06 10:09:35 +08:00
ElyPrism 00af0c281d docs(doc): add docker deployment 2025-10-05 21:34:17 +08:00
ElyPrism 001f206a31 docs(readme): fix markdown grammar 2025-10-05 21:26:40 +08:00
ElyPrism 4d9045d597 docs(env-deploy): add env config 2025-10-05 21:25:08 +08:00
ElyPrism e001de3efa ci(dockerignore): add ignore 2025-10-05 21:19:18 +08:00
ElyPrism 6ae1ccd5e3 docs(readme): fix gammar in this readme file 2025-10-05 21:16:53 +08:00
ElyPrism cb72918c55 docs(readme): add docker deploy method 2025-10-05 21:15:14 +08:00
ElyPrism ba16f2f0ab ci(docker): add version tag to every deployment 2025-10-05 20:58:39 +08:00
ElyPrism a76576af23 ci(fix): fix workflow file conflicts 2025-10-05 20:54:52 +08:00
ElyPrism 72526ba20d ci(docker-deployment): add dockerhub push workflow file 2025-10-05 20:51:46 +08:00
ElyPrism db05eddb9c Merge pull request #18 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/prettier-3.6.2
chore(packages): bump prettier from 2.7.1 to 3.6.2
2025-10-05 20:26:09 +08:00
ElyPrism fb6804364a docs: 更新readme 2025-10-05 20:25:39 +08:00
ElyPrism 9e4626e905 Merge pull request #17 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/mocha-11.7.2
chore(packages): bump mocha from 10.0.0 to 11.7.2
2025-10-02 19:22:02 +08:00
ElyPrism 6c8f7be2a5 Merge branch 'main' into dependabot/npm_and_yarn/prettier-3.6.2 2025-10-01 21:56:02 +08:00
ElyPrism cf97ed45b7 Merge branch 'main' into dependabot/npm_and_yarn/mocha-11.7.2 2025-10-01 21:55:50 +08:00
ElyPrism 1d640a789f Merge pull request #15 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/types/node-24.6.0 2025-10-01 21:55:28 +08:00
ElyPrism 22e47582c4 Merge branch 'main' into dependabot/npm_and_yarn/types/node-24.6.0 2025-10-01 21:54:54 +08:00
ImFurina ed3959f545 docs(playlistSubscribe): 更新该内容文档 2025-10-01 21:53:05 +08:00
dependabot[bot] 771c2f9d52 chore(packages): bump prettier from 2.7.1 to 3.6.2
Bumps [prettier](https://github.com/prettier/prettier) from 2.7.1 to 3.6.2.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.7.1...3.6.2)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.6.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-01 03:55:37 +00:00
dependabot[bot] f5b7fa2d26 chore(packages): bump @types/node from 16.11.19 to 24.6.0
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 16.11.19 to 24.6.0.
- [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: 24.6.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-01 03:54:16 +00:00
dependabot[bot] b798f1383b chore(packages): bump mocha from 10.0.0 to 11.7.2
Bumps [mocha](https://github.com/mochajs/mocha) from 10.0.0 to 11.7.2.
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v10.0.0...v11.7.2)

---
updated-dependencies:
- dependency-name: mocha
  dependency-version: 11.7.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-01 03:54:13 +00:00
ElyPrism 6a1b7d987c Merge pull request #16 from NeteaseCloudMusicApiEnhanced/dependabot/npm_and_yarn/eslint-plugin-html-8.1.3
chore(packages): bump eslint-plugin-html from 7.1.0 to 8.1.3
2025-10-01 11:52:29 +08:00
ElyPrism db95ee1987 Merge pull request #14 from NeteaseCloudMusicApiEnhanced/dependabot/github_actions/actions/cache-4
chore(deps): bump actions/cache from 3 to 4
2025-10-01 11:51:37 +08:00
dependabot[bot] edb9fbba09 chore(deps): bump actions/cache from 3 to 4
Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-01 03:50:53 +00:00
ElyPrism ede6534ebf Merge pull request #13 from NeteaseCloudMusicApiEnhanced/dependabot/github_actions/docker/setup-buildx-action-3
chore(deps): bump docker/setup-buildx-action from 2 to 3
2025-10-01 11:50:48 +08:00
ElyPrism 15c6ea7266 Merge pull request #12 from NeteaseCloudMusicApiEnhanced/dependabot/github_actions/actions/checkout-5
chore(deps): bump actions/checkout from 3 to 5
2025-10-01 11:50:10 +08:00
ElyPrism a719631607 Merge pull request #11 from NeteaseCloudMusicApiEnhanced/dependabot/github_actions/actions/setup-node-5
chore(deps): bump actions/setup-node from 3 to 5
2025-10-01 11:49:24 +08:00
dependabot[bot] e7d9d0fdf2 chore(packages): bump eslint-plugin-html from 7.1.0 to 8.1.3
Bumps [eslint-plugin-html](https://github.com/BenoitZugmeyer/eslint-plugin-html) from 7.1.0 to 8.1.3.
- [Changelog](https://github.com/BenoitZugmeyer/eslint-plugin-html/blob/main/CHANGELOG.md)
- [Commits](https://github.com/BenoitZugmeyer/eslint-plugin-html/compare/v7.1.0...v8.1.3)

---
updated-dependencies:
- dependency-name: eslint-plugin-html
  dependency-version: 8.1.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-09-30 11:57:34 +00:00
dependabot[bot] 27bba9ce55 chore(deps): bump docker/setup-buildx-action from 2 to 3
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-09-30 11:56:36 +00:00
dependabot[bot] 27562092b1 chore(deps): bump actions/checkout from 3 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v5)

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

Signed-off-by: dependabot[bot] <[email protected]>
2025-09-30 11:56:33 +00:00
dependabot[bot] 053640fd33 chore(deps): bump actions/setup-node from 3 to 5
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 5.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v5)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-09-30 11:56:28 +00:00
ElyPrism f7ad6b7d61 ci(bot): enable dependabot 2025-09-30 19:55:50 +08:00
ImFurina da6f590632 chore(package): update dependence 2025-09-30 19:52:17 +08:00
ImFurina 96148385c4 4.29.6 2025-09-30 19:52:01 +08:00
ImFurina 348b7569b8 4.29.5 2025-09-23 20:25:21 +08:00
ImFurina ea3acc0b14 feat(unblock): 新增bodian source 2025-09-23 20:19:47 +08:00
ImFurina 09f6b862fe chore(deps): 更新UNM至0.28.0 2025-09-23 20:12:45 +08:00
ImFurina 0535b88b24 chore(deps): 更新必要的依赖 2025-09-19 19:59:07 +08:00
ImFurina 9fe3a0f491 chores 2025-09-13 14:36:22 +08:00
ImFurinaandbinaryify b73685ace3 chore(deps): 更新axios依赖
Co-Authored-By: binaryify <[email protected]>
2025-09-12 20:37:20 +08:00
ImFurina f16c92e825 chore(version): 更新版本号 2025-09-07 13:13:59 +08:00
ImFurinaandbinaryify abd0c9ca91 refactor: 重构模块加载
Refactored main.js to optimize module and server loading, improving code clarity and lazy loading. Updated several dependencies in package.json to their latest versions for better security and compatibility. Updated documentation cover page and home page to reflect project branding changes.

Co-Authored-By: binaryify <[email protected]>
2025-09-07 13:08:26 +08:00
ImFurinaandbinaryify 4f4aa134d5 perf: 新增接口, 修复登录问题
Co-Authored-By: binaryify <[email protected]>
2025-09-07 12:58:58 +08:00
126 changed files with 12451 additions and 17930 deletions
+2 -1
View File
@@ -7,8 +7,9 @@
!/app.js
!/server.js
!/package.json
!/package-lock.json
!/index.js
!/generateConfig.js
!/main.js
!/data
!/.env
!/pnpm-lock.yaml
+4 -18
View File
@@ -10,29 +10,15 @@ PROXY_URL = "https://your-proxy-url.com/?proxy="
### UnblockNeteaseMusic 设置项
## 启用全局解灰, 无论是否调用参数都会使用解灰(推荐开启)
ENABLE_GENERAL_UNBLOCK = true
## 启用全局解灰, 无论是否调用参数都会使用解灰(不推荐开启)
ENABLE_GENERAL_UNBLOCK = false
## 歌曲启用无损音质
ENABLE_FLAC = true
## 启用无损音质时,是否选择音质最高的
SELECT_MAX_BR = true
## 音源设置
UNBLOCK_SOURCE = pyncmd,qq,kuwo,migu,kugou
SELECT_MAX_BR = false
## 严格按照配置音源设置顺序进行匹配
FOLLOW_SOURCE_ORDER = true
## Cookie设置项; 推荐在Vercel等平台进行环境变量的设置或者设置github secret
# 网易云 cookie
# 格式:MUSIC_U=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NETEASE_COOKIE = ""
# JOOX cookie
# 格式:JOOX_COOKIE="wmid=<your_wmid>; session_key=<your_session_key>"
JOOX_COOKIE = ""
# 咪咕 cookie
# 格式:MIGU_COOKIE="<your_aversionid>"
MIGU_COOKIE = ""
# qq cookie
# 格式:QQ_COOKIE="uin=<your_uin>; qm_keyst=<your_qm_keyst>"
QQ_COOKIE = ""
# Youtube 密钥
# 格式:YOUTUBE_KEY="<your_data_api_key>"
YOUTUBE_KEY = ""
# NETEASE_COOKIE = ""
-57
View File
@@ -1,57 +0,0 @@
module.exports = {
root: true,
ignorePatterns: ['public/'],
parserOptions: {
parser: 'babel-eslint',
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: ['html'],
extends: ['plugin:prettier/recommended'],
env: {
browser: true,
node: true,
},
rules: {
'prettier/prettier': [
'error',
{
endOfLine: 'auto',
},
],
indent: ['error', 2, { SwitchCase: 1 }],
'space-infix-ops': ['error', { int32Hint: false }],
'key-spacing': [
2,
{
beforeColon: false,
afterColon: true,
},
],
'no-octal': 2,
'no-redeclare': 2,
'comma-spacing': 2,
'no-new-object': 2,
'arrow-spacing': 2,
quotes: [
2,
'single',
{
avoidEscape: true,
allowTemplateLiterals: true,
},
],
},
overrides: [
{
files: ['**/*.ts'],
parser: '@typescript-eslint/parser',
extends: [
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
// 'prettier/@typescript-eslint',
],
},
],
}
+1
View File
@@ -0,0 +1 @@
custom: ["https://rewards.focalors.ltd"]
-45
View File
@@ -1,45 +0,0 @@
---
name: 创建问题反馈
about: 问题反馈
title: "<不按照模版格式或者删除模版信息将得不到处理,确认看完了调用前须知再发 issues>"
labels: ''
assignees: ''
---
## 环境
- 系统/平台: <你的系统和平台>
- nodejs 版本: <你的 NodeJS 版本号>
- API版本: <运行的云音乐 API 的版本号, 对应 package.json 里面的 version>
## 出现问题
<请清晰描述出现的问题,不要使用xxx不行之类的模糊不清的语句,我没有读心术>
## 重现步骤
<请清晰描述重现步骤,调用的接口链接和参数以及命令行错误信息,最好贴上截图(命令行错误信息和接口返回内容)>
## 期待效果
<请清晰描述现在的效果和期待的效果>
---
>注意: 如果解灰出现问题, 本项目只是集成[UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server)的接口, 请在对应的仓库开启议题
>维护项目都是业余时间,精力有限,我只能挑容易解决的issues处理,为了节约双方时间,请尽可能提供足够的有用的信息,给的信息不够我只能根据精力和时间看情况处理,如果模板信息看都不看就删掉,我不会进行任何回复,并且一个月后close掉issue
>先看文档有没有相关说明,调用前须知必看,确认看完了调用前须知再发 issues
>先在 issues 搜一下是否有相似问题,没有再发,否则直接关闭
>不处理别人搭建的线上服务的问题,此项目提供任何线上服务不保证质量,请自行搭建
>重现步骤尽量详细,不能含糊不清,包含请求地址和对应参数以及操作过程描述,不是每个人都喜欢猜别人遇到了什么问题和找参数一个个试,也比较浪费时间
>如果不是提建议,提 issues 如果不照着模版来将不会优先处理或放着不管
>460 cheating 的问题把 `utils/request.js` 里面的 `headers['X-Real-IP']` 的注释取消掉就好
-11
View File
@@ -1,11 +0,0 @@
---
name: 功能需求
about: 希望添加新接口
title: ''
labels: 功能需求
assignees: ''
---
**功能描述**
- 请清晰描述你所希望添加的功能,最好贴上对应客户端界面的截图
+132
View File
@@ -0,0 +1,132 @@
name: 🐛 Bug报告
description: 报告API其中的错误或问题
title: "[Bug] "
labels: ["bug"]
assignees: []
body:
- type: markdown
attributes:
value: |
感谢您报告Bug!请填写以下信息帮助我们快速定位和修复问题。
- type: checkboxes
id: terms
attributes:
label: 确认事项
description: 在提交Bug报告前,请确认以下事项
options:
- label: 我已经搜索了现有的issues,确认这不是重复问题
required: true
- label: 我使用的是最新版本的API, 而且是官方发布的版本,而不是fork或修改版
required: true
- label: 不处理别人搭建的线上服务的问题,此项目提供任何线上服务不保证质量
required: true
- label: 如果不是提建议,提 issues 如果不照着模版来将不会优先处理或放着不管
required: true
- label: 维护项目都是业余时间,精力有限,我只能挑容易解决的issues处理,为了节约双方时间,请尽可能提供足够的有用的信息,给的信息不够我只能根据精力和时间看情况处理,如果模板信息看都不看就删掉,我不会进行任何回复,并且一个月后close掉issue
required: true
- type: input
id: version
attributes:
label: API版本
description: 请提供您使用的API版本以及nodejs版本
placeholder: "v4.2x.x, Node.js v22.x.x"
validations:
required: true
- type: dropdown
id: os
attributes:
label: 操作系统或平台
description: 您在哪个操作系统上遇到了这个问题?
options:
- Windows 10
- Windows 11
- Ubuntu 20.04
- Ubuntu 22.04
- macOS
- 其他 Linux 发行版
- 使用部署平台或其他 (请在描述中说明)
validations:
required: true
- type: dropdown
id: deployment
attributes:
label: 部署平台
description:
options:
- 我使用的自己的服务器部署
- Vercel
- Heroku
- Railway
- 其他云平台 (请在描述中说明)
validations:
required: true
- type: textarea
id: bug-description
attributes:
label: Bug描述
description: 请详细描述您遇到的问题
placeholder: |
清晰简洁地描述bug的具体表现...
validations:
required: true
- type: textarea
id: reproduction-steps
attributes:
label: 重现步骤
description: 请提供详细的重现步骤
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: 期望行为
description: 请描述您期望发生的行为
placeholder: 清晰简洁地描述您期望应该发生什么...
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: 实际行为
description: 请描述实际发生的行为
placeholder: 清晰简洁地描述实际发生了什么...
validations:
required: true
- type: textarea
id: error-output
attributes:
label: 错误输出日志
description: 请提供完整的错误信息日志
render: shell
placeholder: |
错误信息输出...
- type: textarea
id: additional-context
attributes:
label: 额外信息
description: 添加任何其他有助于理解问题的信息
placeholder: |
- 截图
- 相关配置文件
- 其他相关信息
- type: checkboxes
id: help
attributes:
label: 愿意协助
description: 您是否愿意帮助修复这个bug?
options:
- label: 我愿意提交PR来修复这个bug
- label: 我可以提供更多测试用例
- label: 我可以帮助测试修复方案
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: 提问的艺术
url: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md
about: 默认所有 Issues 发起者均已了解此处的内容
@@ -0,0 +1,72 @@
name: ✨ 接口添加请求
description: 希望添加新接口
title: "[Feature] "
labels: ["enhancement"]
assignees: []
body:
- type: markdown
attributes:
value: |
感谢您提出新功能建议!请填写以下信息帮助我们理解您的需求。
- type: checkboxes
id: terms
attributes:
label: 确认事项
description: 在提交功能请求前,请确认以下事项
options:
- label: 我已经搜索了现有的issues,确认这不是重复请求
required: true
- label: 我已经查看了项目文档
required: true
- label: 该项目符合该项目的目标和范围
required: true
- label: 我确认我提供的接口请求是合理且有意义的
required: true
- type: textarea
id: proposed-solution
attributes:
label: 功能描述
description: 请详细描述您希望添加的功能,最好贴上对应客户端界面的截图
placeholder: |
请详细描述您期望的功能:
- 新的网易云音乐api 接口?
validations:
required: true
- type: dropdown
id: priority
attributes:
label: 优先级
description: 您认为这个功能的优先级如何?
options:
- 低 (可有可无的改进)
- 中 (会显著改善使用体验)
- 高 (严重影响了我使用)
- 紧急 (阻塞了重要工作)
validations:
required: true
- type: textarea
id: compatibility
attributes:
label: 兼容性考虑
description: 这个功能是否会影响现有代码的兼容性?
placeholder: |
- 是否是破坏性变更?
- 是否需要语法变更?
- 对性能的影响?
- type: checkboxes
id: implementation
attributes:
label: 实现意愿
description: 您是否愿意参与这个功能的实现?
options:
- label: 我愿意提交PR来实现这个功能
- label: 我可以协助设计API接口
- label: 我可以提供测试用例
- label: 我可以帮助编写文档
+41
View File
@@ -0,0 +1,41 @@
# .github/dependabot.yml
version: 2
updates:
# 更新 npm (JavaScript) 依赖
- package-ecosystem: "npm"
directory: "/" # package.json 所在的位置
schedule:
interval: "weekly" # 每周一检查
# 默认情况下,Dependabot 会自动获取最新版本。
# 你可以使用 `ignore` 来排除某些特定更新
ignore:
- dependency-name: "eslint"
# 对于 ESLint,忽略主版本 8 以上的所有更新
versions: ["> 8"]
# 自动合并小版本更新的 PR (可选)
# 可以设置针对 patch 或 minor 版本更新自动合并
commit-message:
prefix: "build(packages):" # PR 合并提交信息的前缀
# 更新 GitHub Actions 依赖
- package-ecosystem: "github-actions"
directory: "/" # .github/workflows/ 目录所在的位置
schedule:
interval: "monthly"
# 更新 Python (pip) 依赖,如果你的项目有 requirements.txt
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
# 如果你有多个目录(例如一个 monorepo),可以为每个目录单独配置
# - package-ecosystem: "npm"
# directory: "/packages/frontend"
# schedule:
# interval: "weekly"
# - package-ecosystem: "npm"
# directory: "/packages/backend"
# schedule:
# interval: "weekly"
+51 -27
View File
@@ -1,31 +1,55 @@
name: Build Image
name: Publish Docker image
on:
push:
tags:
- v*
release:
types: [published]
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
build:
if: github.event.base_ref == 'refs/heads/main'
runs-on: ubuntu-latest
name: Build image job
steps:
- name: Checkout main
uses: actions/checkout@v3
- name: Get version
id: get_version
if: startsWith(github.ref, 'refs/tags/') && startsWith(github.repository, 'Binaryify/ncm')
run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and publish image
uses: elgohr/Publish-Docker-Github-Action@v5
if: startsWith(github.ref, 'refs/tags/') && startsWith(github.repository, 'Binaryify/ncm')
with:
name: binaryify/netease_cloud_music_api # dockerid/imageName
platforms: linux/arm64,linux/amd64 # 你准备构建的镜像平台
tags: latest,${{ steps.get_version.outputs.VERSION }}
username: ${{ secrets.DOCKER_USERNAME }} # docker hub userid 在setting创建secrets name=DOCKER_USERNAME value=dockerid
password: ${{ secrets.DOCKER_PASSWORD }} # docker hub password,在setting创建secrets name=DOCKER_PASSWORD value=dockerpassword
build-and-push:
if: startsWith(github.event.release.tag_name, 'v')
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
# 读取 package.json 的版本号
- name: Read package version
id: pkg
run: echo "VERSION=$(jq -r .version package.json)" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: |
moefurina/ncm-api:latest
moefurina/ncm-api:${{ env.VERSION }}
ghcr.io/neteasecloudmusicapienhanced/ncm-api:latest
ghcr.io/neteasecloudmusicapienhanced/ncm-api:${{ env.VERSION }}
platforms: linux/amd64,linux/arm64/v8
+109
View File
@@ -0,0 +1,109 @@
name: Build and Create PR
on:
workflow_dispatch: # 手动触发
push:
branches: [main]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
include:
- os: ubuntu-latest
platform: linux
target: node18-linux-x64
output: precompiled/app
- os: windows-latest
platform: win
target: node18-win-x64
output: precompiled/app.exe
- os: macos-latest
platform: macos
target: node18-macos-x64
output: precompiled/app
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '18'
- name: Install dependencies
run: |
npm install -g pnpm
pnpm install
- name: Build for ${{ matrix.platform }}
run: |
npm run pkg${{ matrix.platform }}
env:
PKG_TARGET: ${{ matrix.target }}
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: app-${{ matrix.platform }}
path: ${{ matrix.output }}
if-no-files-found: error
create-pr:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' # 只在手动触发时创建PR
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
path: precompiled
- name: Display structure of downloaded files
run: ls -R
- name: Setup Node.js
uses: actions/setup-node@v6
- name: Create new branch and commit
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
BRANCH_NAME="auto-build-$(date +%Y%m%d-%H%M%S)"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
git checkout -b $BRANCH_NAME
# 复制并整理下载的文件
mkdir -p precompiled
cp app-win/* precompiled/ || true
cp app-linux/* precompiled/ || true
cp app-macos/* precompiled/ || true
# 提交更改
git add precompiled/
git commit -m "Auto-build: Add compiled binaries for win, linux, macos" || exit 0
# 推送到远程仓库
git push origin $BRANCH_NAME
- name: Create Pull Request
uses: actions/github-script@v8
with:
script: |
const { data: pullRequest } = await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'Auto-build: Add compiled binaries for win, linux, macos',
head: '${{ env.BRANCH_NAME }}',
base: 'main',
body: 'This PR contains newly built binaries for Windows, Linux, and macOS platforms.'
});
console.log(`Created PR #${pullRequest.number}: ${pullRequest.html_url}`);
+79
View File
@@ -0,0 +1,79 @@
name: Issue Management
permissions:
issues: write
contents: read
on:
issues:
types: [opened, labeled]
issue_comment:
types: [created]
jobs:
welcome-new-issues:
if: github.event.action == 'opened'
runs-on: ubuntu-latest
steps:
- name: Welcome new issue
uses: peter-evans/create-or-update-comment@v5
with:
issue-number: ${{ github.event.issue.number }}
body: |
## 快速链接
在我们处理您的问题之前,您可以先查看以下资源:
- 📖 查看[项目文档](https://neteasecloudmusicapienhanced.js.org)
- 💬 加入[QQ交流群](https://qm.qq.com/q/TpeP9Uv2yk)
- 🔍 搜索[现有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:
if: github.event.action == 'opened'
runs-on: ubuntu-latest
steps:
- name: Mark stale issues
uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: |
这个issue已经30天没有活动了。
如果这个问题仍然存在,请回复此评论说明当前状态。
否则,这个issue将在7天后自动关闭。
您可以随时重新打开已关闭的issue。
stale-pr-message: |
这个PR已经30天没有活动了。
如果您仍在处理这个PR,请回复此评论。
否则,这个PR将在7天后自动关闭。
days-before-stale: 30
days-before-close: 7
stale-issue-label: "stale"
stale-pr-label: "stale"
exempt-issue-labels: "pinned,enhancement,priority:high,ongoing,fatal"
exempt-pr-labels: "pinned,priority:high,ongoing,fatal"
-59
View File
@@ -1,59 +0,0 @@
name: Node.js CI
on:
pull_request:
branches: [ main ]
push:
branches: [ main ]
permissions:
contents: read
jobs:
test:
name: Test
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x ]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
name: Install dependencies
- name: Test
env:
NCM_API_TEST_LOGIN_COUNTRY_CODE: ${{ secrets.NCM_API_TEST_LOGIN_COUNTRY_CODE }}
NCM_API_TEST_LOGIN_PHONE: ${{ secrets.NCM_API_TEST_LOGIN_PHONE }}
NCM_API_TEST_LOGIN_PASSWORD: ${{ secrets.NCM_API_TEST_LOGIN_PASSWORD }}
run: npm test
lint:
name: Lint
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
name: Install dependencies
- name: Lint
run: npm run lint
+56
View File
@@ -0,0 +1,56 @@
name: Node.js CI
on:
pull_request:
branches: [main]
push:
branches: [main]
permissions:
contents: read
jobs:
test:
name: Test
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x, 22.x]
steps:
- uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
# Use built-in package manager cache for pnpm
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Install pnpm
run: npm install -g pnpm
- name: Install dependencies (pnpm)
run: pnpm install --frozen-lockfile
- name: Test
env:
NCM_API_TEST_LOGIN_COUNTRY_CODE: ${{ secrets.NCM_API_TEST_LOGIN_COUNTRY_CODE }}
NCM_API_TEST_LOGIN_PHONE: ${{ secrets.NCM_API_TEST_LOGIN_PHONE }}
NCM_API_TEST_LOGIN_PASSWORD: ${{ secrets.NCM_API_TEST_LOGIN_PASSWORD }}
run: pnpm test
lint:
name: Lint
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
steps:
- uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Install pnpm
run: npm install -g pnpm
- name: Install dependencies (pnpm)
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm run lint
+9 -12
View File
@@ -2,41 +2,38 @@
name: Release
on:
push:
release:
types: [published]
workflow_dispatch:
jobs:
release:
if: startsWith(github.event.release.tag_name, 'v')
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v6
# 发布到 NPM Registry
- name: Setup Node.js for NPM
uses: actions/setup-node@v3
uses: actions/setup-node@v6
with:
node-version: 20
registry-url: 'https://registry.npmjs.org/'
- run: npm install
- name: Publish to NPM
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --provenance --access public
# 发布到 GitHub Packages
- name: Setup Node.js for GitHub Packages
uses: actions/setup-node@v3
uses: actions/setup-node@v6
with:
node-version: 20
registry-url: 'https://npm.pkg.github.com'
- name: Update package name for GitHub Packages
run: |
json=`cat package.json`
echo "$json" | jq '.name = "@neteasecloudmusicapienhanced/api"' > package.json
- name: Publish to GitHub Packages
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ github.token }}
NODE_AUTH_TOKEN: ${{ github.token }}
+2 -2
View File
@@ -17,12 +17,12 @@ jobs:
steps:
# Step 1: run a standard checkout action
- name: Checkout target repo
uses: actions/checkout@v4
uses: actions/checkout@v6
# Step 2: run the sync action
- name: Sync upstream changes
id: sync
uses: aormsby/[email protected].1
uses: aormsby/[email protected].2
with:
upstream_sync_repo: NeteaseCloudMusicApiEnhanced/api-enhanced
upstream_sync_branch: main
+2
View File
@@ -8,3 +8,5 @@ examples/moddef.json
bin
anonymous_token
.vercel
.env
IFLOW.md
View File
+1 -1
View File
@@ -1,4 +1,4 @@
language: node_js
node_js:
- 12
- 22
+26 -1
View File
@@ -1,6 +1,31 @@
# 更新日志
### 4.30.2 | 2026.02.22
- feat(cloud): 添加云盘上传模式选择功能并优化上传配置 (#116)
- refactor: Refactor random Chinese IP generator (#110)
- fix(server): Correctly inject client IP into request parameters (#108)
- refactor(ui): More elegant display of URL and debug buttons (#109)
- feat: 添加评论统计数据 (#122)
## 二开作者注: 这些`commit`记录为原版网易云音乐API的记录, 本项目不会对其进行添加以及修改
### 4.30.1 | 2026.02.11
- feat: Add user playlist endpoints & domain overrides (#105)
### 4.30.0 | 2026.02.06
- feat: 新增音乐人黑胶会员任务接口 `/musician/vip/tasks` (#95)
- feat: 自动构建: 添加Windows、Linux、macOS预编译二进制文件 (#88)
- fix: 修复模块未定义问题
- chore: 更新依赖项 (music-metadata: ^11.11.1 -> ^11.11.2, ansi-escapes: ^7.2.0 -> ^7.3.0, commander: ^14.0.2 -> ^14.0.3)
- chore: 更新GitHub Actions (checkout: v4 -> v6, setup-node: v4 -> v6, upload-artifact: v4 -> v6, download-artifact: v4 -> v7, github-script: v7 -> v8)
- refactor: 注释掉IP地址日志输出以提升隐私保护
- refactor: 重构前端测试页面, 主要改进:
- 统一使用简洁现代的设计风格
- 去除渐变背景和复杂动画
- 使用纯色背景(#f5f5f5)和白色卡片
- 优化表单布局和交互体验
- 增强错误处理和加载状态
- 移除第三方框架依赖(Tailwind、Bootstrap、MDUI)
- 升级Vue 2到Vue 3
- 将硬编码的配置项移至前端表单
- 所有页面现在都保持一致的设计语言,简洁清爽,功能完整。
### 4.25.0 | 2024.11.16
- feat: 增加副歌时间、相关歌单推荐接口,原有相关歌单接口已废弃;fix: 将部分易盾白名单接口替换为eapi [#30](https://gitlab.com/Binaryify/neteasecloudmusicapi/-/merge_requests/30)
+150 -21
View File
@@ -1,16 +1,51 @@
# 网易云音乐 API Enhanced
<p align="center">
<img src="./public/static/neteaselogo.png" alt="网易云音乐 API Enhanced Logo" />
</p>
<p align="center">
<a href="https://www.npmjs.com/package/@neteasecloudmusicapienhanced/api">
<img src="https://img.shields.io/npm/v/@neteasecloudmusicapienhanced/api?style=flat-square" alt="NPM Version" />
</a>
<a href="https://www.npmjs.com/package/@neteasecloudmusicapienhanced/api">
<img src="https://img.shields.io/npm/dm/@neteasecloudmusicapienhanced/api?style=flat-square" alt="NPM Downloads" />
</a>
<a href="https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced">
<img src="https://img.shields.io/github/license/NeteaseCloudMusicApiEnhanced/api-enhanced?style=flat-square" alt="License" />
</a>
<a href="https://nodejs.org/">
<img src="https://img.shields.io/node/v/@neteasecloudmusicapienhanced/api?style=flat-square" alt="Node Version" />
</a>
<a href="https://hub.docker.com/r/moefurina/ncm-api">
<img src="https://img.shields.io/docker/v/moefurina/ncm-api?style=flat-square" alt="Docker Version" />
</a>
<a href="https://hub.docker.com/r/moefurina/ncm-api">
<img src="https://img.shields.io/docker/pulls/moefurina/ncm-api?style=flat-square" alt="Docker Pulls" />
</a>
<a href="https://www.typescriptlang.org/">
<img src="https://img.shields.io/badge/TypeScript-Ready-blue?style=flat-square&logo=typescript" alt="TypeScript" />
</a>
</p>
---
## 项目简介
本项目为网易云音乐第三方 Node.js API,基于社区停更的原版 API 持续维护和升级,支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署(如果原版诈尸, 我会及时同步 or 归档)。
网易云音乐第三方 Node.js API, 支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署(如果原版诈尸, 我会及时同步 or 归档)。
> [!IMPORTANT]
>
> ## 注意
>
> - 本项目是自由项目, 不带任何担保, 在基于`MIT License`的许可下, 任何人都可以自由使用、修改和分发本项目的代码, 包括用于商业目的.
> - 该项目是个合作计划, 有很多人参与了此次贡献, 前往 [Contributors](https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced/graphs/contributors) 页面查看所有贡献者
> - 原作者项目 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 并非完全停止维护, 你可以在 [NeteaseCloudMusicApi 的 NPMJS 页面](https://www.npmjs.com/package/NeteaseCloudMusicApi) 查看最新版本信息。
> - 感谢尊重和理解!
## 快速开始
### 环境要求
- Node.js 14 及以上
- Node.js 18 及以上
- 推荐使用 pnpm 进行依赖管理
### 安装
@@ -34,25 +69,92 @@ set PORT=4000 && node app.js # Windows
### 重要提示
- 调用前请务必阅读文档的 [调用前须知](https://music-api.focalors.ltd/#/?id=%e8%b0%83%e7%94%a8%e5%89%8d%e9%a1%bb%e7%9f%a5) 部分。
- 调用前请务必阅读文档的 [调用前须知](https://neteasecloudmusicapienhanced.js.org/#/?id=%e8%b0%83%e7%94%a8%e5%89%8d%e9%a1%bb%e7%9f%a5) 部分。
- 推荐将敏感信息(如 cookie)通过部署平台的环境变量进行配置。
## 在线体验与文档
- [在线文档](https://music-api.focalors.ltd/)
- [NPM 包地址](https://www.npmjs.com/package/@neteaseapireborn/api)
- [在线文档](https://neteasecloudmusicapienhanced.js.org/)
- [NPM 包地址](https://www.npmjs.com/package/@neteasecloudmusicapienhanced/api)
- [Dockerhub 主页](https://hub.docker.com/r/moefurina/ncm-api)
## 常见部署方式
- 推荐参考[在线文档](https://music-api.focalors.ltd/)
- 推荐参考[在线文档](https://neteasecloudmusicapienhanced.js.org/)
### Vercel 一键部署
## Docker 部署说明
本项目支持通过 Docker 一键部署,无需手动安装 Node.js 或依赖。
> 注意: 在 docker 中运行的时候, 由于使用了 request 来发请求, 所以会检查几个 proxy 相关的环境变量(如下所列), 这些环境变量 会影响到 request 的代理, 详情请参考[request 的文档](https://github.com/request/request#proxies), 如果这些环境变量 指向的代理不可用, 那么就会造成错误, 所以在使用 docker 的时候一定要注意这些环境变量. 不过, 要是你在 query 中加上了 proxy 参数, 那么环境变量会被覆盖, 就会用你通过 proxy 参数提供的代理了.
request 相关的环境变量
1. http_proxy
2. https_proxy
3. HTTP_PROXY
4. HTTPS_PROXY
5. no_proxy
6. NO_PROXY
```shell
docker pull moefurina/ncm-api:latest
docker run -d -p 3000:3000 --name ncm-api moefurina/ncm-api:latest
// 或者
docker run -d -p 3000:3000 moefurina/ncm-api:latest
// 去掉或者设置相关的环境变量
docker run -d -p 3000:3000 --name ncm-api -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= moefurina/ncm-api:latest
// 或者
docker run -d -p 3000:3000 -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= moefurina/ncm-api:latest
```
> 以下是自行 build docker 镜像方式
```shell
$ git clone https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced && cd api-enhanced
$ sudo docker build . -t ncm-api
$ sudo docker run -d -p 3000:3000 ncm-api
```
## 3. 环境变量
| 变量名 | 默认值 | 说明 |
|----------------------------|--------------------------------------|----------------------------------------------------|
| **CORS_ALLOW_ORIGIN** | `*` | 允许跨域请求的域名。可填写单个源,或使用逗号分隔多个源(例如 `https://a.com,https://b.com`)。 |
| **ENABLE_PROXY** | `false` | 是否启用反向代理功能。 |
| **PROXY_URL** | `https://your-proxy-url.com/?proxy=` | 代理服务地址。仅当 `ENABLE_PROXY=true` 时生效。 |
| **ENABLE_GENERAL_UNBLOCK** | `true` | 是否启用全局解灰(推荐开启)。开启后所有歌曲都尝试自动解锁。 |
| **ENABLE_FLAC** | `true` | 是否启用无损音质(FLAC)。 |
| **SELECT_MAX_BR** | `false` | 启用无损音质时,是否选择最高码率音质。 |
| **FOLLOW_SOURCE_ORDER** | `true` | 是否严格按照音源列表顺序进行匹配。 |
---
### 4. 更新镜像
更新到最新版本:
```bash
docker pull moefurina/ncm-api:latest
docker stop ncm-api && docker rm ncm-api
docker run -d -p 3000:3000 moefurina/ncm-api:latest
```
## Vercel 一键部署
1. fork 本项目
2. 在 Vercel 官网新建项目,导入 fork 的仓库
3. 直接 Deploy
### 腾讯云 Serverless 部署
## 腾讯云 Serverless 部署
1. fork 本项目
2. 在腾讯云 serverless 控制台新建 Web 应用,选择 Express 框架
@@ -69,7 +171,10 @@ set PORT=4000 && node app.js # Windows
支持直接在 Node.js 项目中引入和调用,返回 Promise:
```js
const { login_cellphone, user_cloud } = require('@neteaseapireborn/api')
const {
login_cellphone,
user_cloud,
} = require('@neteasecloudmusicapienhanced/api')
async function main() {
const result = await login_cellphone({ phone: '手机号', password: '密码' })
console.log(result)
@@ -82,7 +187,7 @@ main()
## TypeScript 支持
```ts
import { banner } from '@neteaseapireborn/api'
import { banner } from '@neteasecloudmusicapienhanced/api'
banner({ type: 0 }).then((res) => console.log(res))
```
@@ -98,22 +203,46 @@ pnpm test
- 用户信息、歌单、动态、播放记录
- 歌曲、专辑、歌手、MV、歌词、评论、排行榜
- 搜索、推荐、私人 FM、签到、云盘
- 歌曲解锁(解灰)、多音源支持(qq/kuwo/kugou/migu/pyncmd)
> 注意: 如果解灰出现问题, 本项目只是集成[UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server)的接口, 请在对应的仓库开启议题
- 详细接口请见[在线文档](https://music-api.focalors.ltd/)
- 歌曲解锁(解灰)
## 贡献与社区
- 欢迎提交 PR、Issue 参与维护
- 要贡献新接口, 请参考[这篇文章](https://www.focalors.ltd/post/how-to-contribute-ncm-api)
## SDK 生态
### 致谢
| 语言 | 作者 | 地址 | 类型 |
| ------ | ------------------------------------------- | ---------------------------------------------------------------------------------------- | ------ |
原作者 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 项目为本项目基础 (该项目在`npmjs`网站上仍持续维护, 但 github 仓库已不再更新)
感谢大佬们为逆向eapi, weapi等加密算法所做的贡献
项目参考:
[trazyn/ieaseMusic](https://github.com/trazyn/ieaseMusic)
[listen1/listen1_chrome_extension](https://github.com/listen1/listen1_chrome_extension)
[chaunsin/netease-cloud-music](https://github.com/chaunsin/netease-cloud-music)
### SDK 生态
| 语言 | 作者 | 地址 | 类型 |
|--------|---------------------------------------------|------------------------------------------------------------------------------------------|-----|
| Java | [JackuXL](https://github.com/JackuXL) | [NeteaseCloudMusicApi-SDK](https://github.com/JackuXL/NeteaseCloudMusicApi-SDK) | 第三方 |
| Java | [1015770492](https://github.com/1015770492) | https://github.com/1015770492/yumbo-music-utils | 第三方 |
| Python | [盧瞳](https://github.com/2061360308) | [NeteaseCloudMusic_PythonSDK](https://github.com/2061360308/NeteaseCloudMusic_PythonSDK) | 第三方 |
| Java | [1015770492](https://github.com/1015770492) | [yumbo-music-utils](https://github.com/1015770492/yumbo-music-utils) | 第三方 |
| Python | [盧瞳](https://github.com/2061360308) | [NeteaseCloudMusic_PythonSDK](https://github.com/2061360308/NeteaseCloudMusic_PythonSDK) | 第三方 |
| Swift | [Lincb522](https://github.com/Lincb522) | [NeteaseCloudMusicApi-Swift](https://github.com/Lincb522/NeteaseCloudMusicAPI-Swift) | 第三方 |
| Rust | [SPlayer-Dev](https://github.com/splayer-dev) | [ncm-api-rs](https://github.com/SPlayer-Dev/ncm-api-rs) | 第三方 |
### 依赖此项目的优秀开源项目
- [qier2222/YesPlayMusic](https://github.com/qier222/YesPlayMusic)
- [MaigoLabs/amaoke.app](https://github.com/MaigoLabs/amaoke.app)
- [Yueby/music-together](https://github.com/Yueby/music-together)
## License
[MIT License](https://github.com/IamFurina/NeteaseCloudMusicApiReborn/blob/main/LICENSE)
[MIT License](https://github.com/MoeFurina/NeteaseCloudMusicApiEnhanced/blob/main/LICENSE)
File diff suppressed because it is too large Load Diff
-14
View File
@@ -1,14 +0,0 @@
{
"functions": [
{
"path": "/(.*)",
"method": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"handler": "index.js",
"headers": {
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version"
}
}
]
}
+91
View File
@@ -0,0 +1,91 @@
const { defineConfig, globalIgnores } = require('eslint/config')
const html = require('eslint-plugin-html')
const globals = require('globals')
const tsParser = require('@typescript-eslint/parser')
const js = require('@eslint/js')
const { FlatCompat } = require('@eslint/eslintrc')
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
})
module.exports = defineConfig([
{
languageOptions: {
ecmaVersion: 2020,
sourceType: 'module',
parserOptions: {
parser: 'babel-eslint',
},
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
html,
},
extends: compat.extends('plugin:prettier/recommended'),
rules: {
'prettier/prettier': [
'error',
{
endOfLine: 'auto',
},
],
indent: [
'error',
2,
{
SwitchCase: 1,
},
],
'space-infix-ops': [
'error',
{
int32Hint: false,
},
],
'key-spacing': [
2,
{
beforeColon: false,
afterColon: true,
},
],
'no-octal': 2,
'no-redeclare': 2,
'comma-spacing': 2,
'no-new-object': 2,
'arrow-spacing': 2,
quotes: [
2,
'single',
{
avoidEscape: true,
allowTemplateLiterals: true,
},
],
},
},
globalIgnores(['**/public/']),
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
},
extends: compat.extends('plugin:@typescript-eslint/recommended'),
},
])
+1
View File
@@ -0,0 +1 @@
import './app.js'
+10
View File
@@ -2,6 +2,7 @@ export interface RequestBaseConfig {
cookie?: string
realIP?: string // IPv4/IPv6 filled in X-Real-IP
proxy?: string // HTTP proxy
randomCNIP?: boolean // Whether to use a random Chinese IP address filled in X-Real-IP
}
export interface MultiPageConfig {
@@ -1165,6 +1166,13 @@ export function song_url_v1(
params: { id: string | number; level: SoundQualityType } & RequestBaseConfig,
): Promise<Response>
export function song_copyright_rcmd(
params: {
songid?: string | number
id?: string | number
} & RequestBaseConfig,
): Promise<Response>
export function top_album(
params: {
area?: AlbumListArea
@@ -1714,6 +1722,8 @@ export function nickname_check(
export function musician_tasks_new(params: RequestBaseConfig): Promise<Response>
export function musician_vip_tasks(params: RequestBaseConfig): Promise<Response>
export function playlist_update_playcount(
params: {
id?: number | string
-29
View File
@@ -1,29 +0,0 @@
## 环境
- 系统/平台: <你的系统和平台>
- nodejs 版本: <你的 NodeJS 版本号>
- API版本:<运行的云音乐 API 的版本号, 对应 package.json 里面的 version>
## 出现问题
<出现的问题>
## 重现步骤
<重现步骤>
## 期待效果
<现在的效果,期待的效果>
>先看文档有没有相关说明,调用前须知必看
>先在 issues 搜一下是否有相似问题,没有再发,否则直接关闭
>不处理别人搭建的线上服务的问题,此项目不提供任何线上服务,请自行搭建
>重现步骤尽量详细,不能含糊不清,包含请求地址和对应参数以及操作过程描述,不是每个人都喜欢猜别人遇到了什么问题和找参数一个个试,也比较浪费时间
>如果不是提建议,提 issues 如果不照着模版来将不会优先处理或直接关闭
>460 cheating 的问题把 `util/request.js` 里面的 `headers['X-Real-IP']` 的注释取消掉就好
+46 -32
View File
@@ -3,47 +3,61 @@ const path = require('path')
const tmpPath = require('os').tmpdir()
const { cookieToJson } = require('./util')
if (!fs.existsSync(path.resolve(tmpPath, 'anonymous_token'))) {
fs.writeFileSync(path.resolve(tmpPath, 'anonymous_token'), '', 'utf-8')
const anonymousTokenPath = path.resolve(tmpPath, 'anonymous_token')
if (!fs.existsSync(anonymousTokenPath)) {
fs.writeFileSync(anonymousTokenPath, '', 'utf-8')
}
let firstRun = true
/** @type {Record<string, any>} */
let obj = {}
fs.readdirSync(path.join(__dirname, 'module'))
.reverse()
.forEach((file) => {
if (!file.endsWith('.js')) return
let fileModule = require(path.join(__dirname, 'module', file))
let fn = file.split('.').shift() || ''
obj[fn] = function (data = {}) {
if (typeof data.cookie === 'string') {
data.cookie = cookieToJson(data.cookie)
}
return fileModule(
{
...data,
cookie: data.cookie ? data.cookie : {},
},
async (...args) => {
if (firstRun) {
firstRun = false
const generateConfig = require('./generateConfig')
await generateConfig()
}
// 待优化
const request = require('./util/request')
return request(...args)
},
)
}
})
const modulePath = path.join(__dirname, 'module')
const moduleFiles = fs.readdirSync(modulePath).reverse()
let requestModule = null
moduleFiles.forEach((file) => {
if (!file.endsWith('.js')) return
const filePath = path.join(modulePath, file)
let fileModule = require(filePath)
let fn = file.split('.').shift() || ''
obj[fn] = function (data = {}) {
const cookie =
typeof data.cookie === 'string'
? cookieToJson(data.cookie)
: data.cookie || {}
return fileModule(
{
...data,
cookie,
},
async (...args) => {
if (!requestModule) {
requestModule = require('./util/request')
}
return requestModule(...args)
},
)
}
})
let serverModule = null
/**
* @type {Record<string, any> & import("./server")}
*/
module.exports = {
...require('./server'),
get server() {
if (!serverModule) {
serverModule = require('./server')
}
return serverModule
},
...obj,
}
Object.assign(module.exports, require('./server'))
+1
View File
@@ -4,6 +4,7 @@ const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
ctcode: query.ctcode || '86',
secrete: 'music_middleuser_pclogin',
cellphone: query.phone,
}
return request(`/api/sms/captcha/sent`, data, createOption(query, 'weapi'))
+141 -123
View File
@@ -1,24 +1,26 @@
const mm = require('music-metadata')
const uploadPlugin = require('../plugins/songUpload')
const md5 = require('md5')
const createOption = require('../util/option.js')
const logger = require('../util/logger.js')
const {
isTempFile,
getFileSize,
getFileMd5,
cleanupTempFile,
getFileExtension,
sanitizeFilename,
} = require('../util/fileHelper')
let mm
module.exports = async (query, request) => {
let ext = 'mp3'
// if (query.songFile.name.indexOf('flac') > -1) {
// ext = 'flac'
// }
if (query.songFile.name.includes('.')) {
ext = query.songFile.name.split('.').pop()
}
mm = require('music-metadata')
query.songFile.name = Buffer.from(query.songFile.name, 'latin1').toString(
'utf-8',
)
const filename = query.songFile.name
.replace('.' + ext, '')
.replace(/\s/g, '')
.replace(/\./g, '_')
const ext = getFileExtension(query.songFile.name)
const filename = sanitizeFilename(query.songFile.name)
const bitrate = 999000
if (!query.songFile) {
return Promise.reject({
status: 500,
@@ -28,119 +30,135 @@ module.exports = async (query, request) => {
},
})
}
if (!query.songFile.md5) {
// 命令行上传没有md5和size信息,需要填充
query.songFile.md5 = md5(query.songFile.data)
query.songFile.size = query.songFile.data.byteLength
}
const res = await request(
`/api/cloud/upload/check`,
{
bitrate: String(bitrate),
ext: '',
length: query.songFile.size,
md5: query.songFile.md5,
songId: '0',
version: 1,
},
createOption(query),
)
let artist = ''
let album = ''
let songName = ''
const useTemp = isTempFile(query.songFile)
let fileSize = await getFileSize(query.songFile)
let fileMd5 = await getFileMd5(query.songFile)
query.songFile.md5 = fileMd5
query.songFile.size = fileSize
try {
const metadata = await mm.parseBuffer(
query.songFile.data,
query.songFile.mimetype,
const res = await request(
`/api/cloud/upload/check`,
{
bitrate: String(bitrate),
ext: '',
length: fileSize,
md5: fileMd5,
songId: '0',
version: 1,
},
createOption(query),
)
const info = metadata.common
if (info.title) {
songName = info.title
}
if (info.album) {
album = info.album
}
if (info.artist) {
artist = info.artist
}
// if (metadata.native.ID3v1) {
// metadata.native.ID3v1.forEach((item) => {
// // logger.info(item.id, item.value)
// if (item.id === 'title') {
// songName = item.value
// }
// if (item.id === 'artist') {
// artist = item.value
// }
// if (item.id === 'album') {
// album = item.value
// }
// })
// // logger.info({
// // songName,
// // album,
// // songName,
// // })
// }
// logger.info({
// songName,
// album,
// songName,
// })
} catch (error) {
logger.info(error)
}
const tokenRes = await request(
`/api/nos/token/alloc`,
{
bucket: '',
ext: ext,
filename: filename,
local: false,
nos_product: 3,
type: 'audio',
md5: query.songFile.md5,
},
createOption(query),
)
let artist = ''
let album = ''
let songName = ''
if (res.body.needUpload) {
const uploadInfo = await uploadPlugin(query, request)
// logger.info('uploadInfo', uploadInfo.body.result.resourceId)
}
// logger.info(tokenRes.body.result)
const res2 = await request(
`/api/upload/cloud/info/v2`,
{
md5: query.songFile.md5,
songid: res.body.songId,
filename: query.songFile.name,
song: songName || filename,
album: album || '未知专辑',
artist: artist || '未知艺术家',
bitrate: String(bitrate),
resourceId: tokenRes.body.result.resourceId,
},
createOption(query),
)
// logger.info({ res2, privateCloud: res2.body.privateCloud })
// logger.info(res.body.songId, 'songid')
const res3 = await request(
`/api/cloud/pub/v2`,
{
songid: res2.body.songId,
},
createOption(query),
)
// logger.info({ res3 })
return {
status: 200,
body: {
...res.body,
...res3.body,
// ...uploadInfo,
},
cookie: res.cookie,
try {
let metadata
if (useTemp) {
metadata = await mm.parseFile(query.songFile.tempFilePath)
} else {
metadata = await mm.parseBuffer(
query.songFile.data,
query.songFile.mimetype,
)
}
const info = metadata.common
if (info.title) songName = info.title
if (info.album) album = info.album
if (info.artist) artist = info.artist
} catch (error) {
logger.info('元数据解析错误:', error.message)
}
const tokenRes = await request(
`/api/nos/token/alloc`,
{
bucket: '',
ext: ext,
filename: filename,
local: false,
nos_product: 3,
type: 'audio',
md5: fileMd5,
},
createOption(query),
)
if (!tokenRes.body.result || !tokenRes.body.result.resourceId) {
logger.error('Token分配失败:', tokenRes.body)
return Promise.reject({
status: 500,
body: {
code: 500,
msg: '获取上传token失败',
detail: tokenRes.body,
},
})
}
if (res.body.needUpload) {
logger.info('需要上传,开始上传流程...')
try {
const uploadInfo = await uploadPlugin(query, request)
logger.info('上传完成:', uploadInfo?.body?.result?.resourceId)
} catch (uploadError) {
logger.error('上传失败:', uploadError)
return Promise.reject(uploadError)
}
} else {
logger.info('文件已存在,跳过上传')
}
const res2 = await request(
`/api/upload/cloud/info/v2`,
{
md5: fileMd5,
songid: res.body.songId,
filename: query.songFile.name,
song: songName || filename,
album: album || '未知专辑',
artist: artist || '未知艺术家',
bitrate: String(bitrate),
resourceId: tokenRes.body.result.resourceId,
},
createOption(query),
)
if (res2.body.code !== 200) {
logger.error('云盘信息上传失败:', res2.body)
return Promise.reject({
status: res2.status || 500,
body: {
code: res2.body.code || 500,
msg: res2.body.msg || '上传云盘信息失败',
detail: res2.body,
},
})
}
const res3 = await request(
`/api/cloud/pub/v2`,
{
songid: res2.body.songId,
},
createOption(query),
)
return {
status: 200,
body: {
...res.body,
...res3.body,
},
cookie: res.cookie,
}
} finally {
if (useTemp) {
await cleanupTempFile(query.songFile.tempFilePath)
}
}
}
+11
View File
@@ -0,0 +1,11 @@
// 获取云盘歌词
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
userId: query.uid,
songId: query.sid,
lv: -1,
kv: -1,
}
return request(`/api/cloud/lyric/get`, data, createOption(query, 'eapi'))
}
+72
View File
@@ -0,0 +1,72 @@
const createOption = require('../util/option.js')
module.exports = async (query, request) => {
const {
songId,
resourceId,
md5,
filename,
song,
artist,
album,
bitrate = 999000,
} = query
if (!songId || !resourceId || !md5 || !filename) {
return Promise.reject({
status: 400,
body: {
code: 400,
msg: '缺少必要参数: songId, resourceId, md5, filename',
},
})
}
const songName = song || filename.replace(/\.[^.]+$/, '')
const res2 = await request(
`/api/upload/cloud/info/v2`,
{
md5: md5,
songid: songId,
filename: filename,
song: songName,
album: album || '未知专辑',
artist: artist || '未知艺术家',
bitrate: String(bitrate),
resourceId: resourceId,
},
createOption(query),
)
if (res2.body.code !== 200) {
return Promise.reject({
status: res2.status || 500,
body: {
code: res2.body.code || 500,
msg: res2.body.msg || '上传云盘信息失败',
detail: res2.body,
},
})
}
const res3 = await request(
`/api/cloud/pub/v2`,
{
songid: res2.body.songId,
},
createOption(query),
)
return {
status: 200,
body: {
code: 200,
data: {
songId: res2.body.songId,
...res3.body,
},
},
cookie: res2.cookie,
}
}
+111
View File
@@ -0,0 +1,111 @@
const { default: axios } = require('axios')
const createOption = require('../util/option.js')
module.exports = async (query, request) => {
const { md5, fileSize, filename, bitrate = 999000 } = query
if (!md5 || !fileSize || !filename) {
return Promise.reject({
status: 400,
body: {
code: 400,
msg: '缺少必要参数: md5, fileSize, filename',
},
})
}
const ext = filename.includes('.') ? filename.split('.').pop() : 'mp3'
const checkRes = await request(
`/api/cloud/upload/check`,
{
bitrate: String(bitrate),
ext: '',
length: fileSize,
md5: md5,
songId: '0',
version: 1,
},
createOption(query),
)
const bucket = 'jd-musicrep-privatecloud-audio-public'
const tokenRes = await request(
`/api/nos/token/alloc`,
{
bucket: bucket,
ext: ext,
filename: filename
.replace(/\.[^.]+$/, '')
.replace(/\s/g, '')
.replace(/\./g, '_'),
local: false,
nos_product: 3,
type: 'audio',
md5: md5,
},
createOption(query, 'weapi'),
)
if (!tokenRes.body.result || !tokenRes.body.result.objectKey) {
return Promise.reject({
status: 500,
body: {
code: 500,
msg: '获取上传token失败',
detail: tokenRes.body,
},
})
}
let lbs
try {
lbs = (
await axios({
method: 'get',
url: `https://wanproxy.127.net/lbs?version=1.0&bucketname=${bucket}`,
timeout: 10000,
})
).data
} catch (error) {
return Promise.reject({
status: 500,
body: {
code: 500,
msg: '获取上传服务器地址失败',
detail: error.message,
},
})
}
if (!lbs || !lbs.upload || !lbs.upload[0]) {
return Promise.reject({
status: 500,
body: {
code: 500,
msg: '获取上传服务器地址无效',
detail: lbs,
},
})
}
return {
status: 200,
body: {
code: 200,
data: {
needUpload: checkRes.body.needUpload,
songId: checkRes.body.songId,
uploadToken: tokenRes.body.result.token,
objectKey: tokenRes.body.result.objectKey,
resourceId: tokenRes.body.result.resourceId,
uploadUrl: `${lbs.upload[0]}/${bucket}/${tokenRes.body.result.objectKey.replace(/\//g, '%2F')}?offset=0&complete=true&version=1.0`,
bucket: bucket,
md5: md5,
fileSize: fileSize,
filename: filename,
},
},
cookie: checkRes.cookie,
}
}
+30
View File
@@ -0,0 +1,30 @@
// 评论统计数据
// type: 0=歌曲 1=MV 2=歌单 3=专辑 4=电台节目 5=视频 6=动态 7=电台
// ids: 资源 ID 列表,多个用逗号分隔,如 "123,456"
const { resourceTypeMap } = require('../util/config.json')
const createOption = require('../util/option.js')
// 从 resourceTypeMap 的前缀值中提取网易云内部资源类型编号
// 例如 "R_SO_4_" -> "4", "A_DR_14_" -> "14"
const resourceTypeIdMap = Object.fromEntries(
Object.entries(resourceTypeMap).map(([key, prefix]) => [
key,
prefix.replace(/_$/, '').split('_').pop(),
]),
)
module.exports = (query, request) => {
const ids = String(query.ids || query.id || '')
.split(',')
.map((id) => id.trim())
.filter(Boolean)
return request(
`/api/resource/commentInfo/list`,
{
resourceType: resourceTypeIdMap[String(query.type || 0)],
resourceIds: JSON.stringify(ids),
},
createOption(query, 'weapi'),
)
}
+11
View File
@@ -0,0 +1,11 @@
// 举报评论
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
threadId: 'R_SO_4_' + query.id,
commentId: query.cid,
reason: query.reason,
}
return request(`/api/report/reportcomment`, data, createOption(query))
}
+9
View File
@@ -0,0 +1,9 @@
// DIFM电台 - 分类
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
sources: query.sources || '[0]',
}
return request(`/api/dj/difm/all/style/channel/v2`, data, createOption(query))
}
+9
View File
@@ -0,0 +1,9 @@
// DIFM电台 - 收藏频道
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
id: query.id,
}
return request(`/api/dj/difm/channel/subscribe`, data, createOption(query))
}
+9
View File
@@ -0,0 +1,9 @@
// DIFM电台 - 取消收藏频道
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
id: query.id,
}
return request(`/api/dj/difm/channel/unsubscribe`, data, createOption(query))
}
+11
View File
@@ -0,0 +1,11 @@
// DIFM电台 - 播放列表
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
limit: query.limit || 5,
source: query.source || 0,
channelId: query.channelId,
}
return request(`/api/dj/difm/playing/tracks/list`, data, createOption(query))
}
+13
View File
@@ -0,0 +1,13 @@
// DIFM电台 - 收藏列表
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
sources: query.sources || '[0]',
}
return request(
`/api/dj/difm/subscribe/channels/get/v2`,
data,
createOption(query),
)
}
+2 -2
View File
@@ -13,12 +13,12 @@ module.exports = async (query, request) => {
[query.captcha ? 'captcha' : 'password']: query.captcha
? query.captcha
: query.md5_password || CryptoJS.MD5(query.password).toString(),
rememberLogin: 'true',
remember: 'true',
}
let result = await request(
`/api/w/login/cellphone`,
data,
createOption(query),
createOption(query, 'weapi'),
)
if (result.body.code === 200) {
+14 -3
View File
@@ -1,9 +1,20 @@
const QRCode = require('qrcode')
const { generateChainId } = require('../util/index')
const createOption = require('../util/option.js')
module.exports = (query, request) => {
module.exports = (query) => {
return new Promise(async (resolve) => {
const url = `https://music.163.com/login?codekey=${query.key}`
const platform = query.platform || 'pc'
const cookie = query.cookie || ''
// 构建基础URL
let url = `https://music.163.com/login?codekey=${query.key}`
// 如果是web平台,则添加chainId参数
if (platform === 'web') {
const chainId = generateChainId(cookie)
url += `&chainId=${chainId}`
}
return resolve({
code: 200,
status: 200,
+11
View File
@@ -0,0 +1,11 @@
// 获取音乐人任务
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {}
return request(
`/api/nmusician/workbench/special/right/vip/info`,
data,
createOption(query, 'eapi'),
)
}
+11
View File
@@ -0,0 +1,11 @@
// 歌单分类列表
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
cat: query.cat || '全部',
limit: query.limit || 24,
newStyle: true,
}
return request(`/api/playlist/category/list`, data, createOption(query))
}
+1 -1
View File
@@ -2,5 +2,5 @@
const createOption = require('../util/option.js')
module.exports = (query, request) => {
return request(`/api/playlist/catalogue`, {}, createOption(query, 'weapi'))
return request(`/api/playlist/catalogue`, {}, createOption(query, 'eapi'))
}
+7 -3
View File
@@ -1,10 +1,14 @@
// 收藏与取消收藏歌单
const { APP_CONF } = require('../util/config.json')
const createOption = require('../util/option.js')
module.exports = (query, request) => {
query.t = query.t == 1 ? 'subscribe' : 'unsubscribe'
const path = query.t == 1 ? 'subscribe' : 'unsubscribe'
const data = {
id: query.id,
...(query.t === 1
? { checkToken: query.checkToken || APP_CONF.checkToken }
: {}),
}
return request(`/api/playlist/${query.t}`, data, createOption(query, 'weapi'))
query.checkToken = true // 强制开启checkToken
return request(`/api/playlist/${path}`, data, createOption(query, 'eapi'))
}
+9
View File
@@ -0,0 +1,9 @@
// 跑步漫游
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
bpm: query.bpm || 50,
}
return request(`/api/radio/sport/get`, data, createOption(query))
}
+3 -1
View File
@@ -2,7 +2,9 @@
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {}
const data = {
afresh: query.afresh,
}
return request(
`/api/v3/discovery/recommend/songs`,
data,
+7 -9
View File
@@ -2,17 +2,14 @@ const CryptoJS = require('crypto-js')
const path = require('path')
const fs = require('fs')
const ID_XOR_KEY_1 = '3go8&$8*3*3h0k(2)2'
const deviceidText = fs.readFileSync(
path.resolve(__dirname, '../data/deviceid.txt'),
'utf-8',
)
const logger = require('../util/logger.js')
const createOption = require('../util/option.js')
const deviceidList = deviceidText.split('\n')
const { generateDeviceId } = require('../util/index')
function getRandomFromList(list) {
return list[Math.floor(Math.random() * list.length)]
}
// function getRandomFromList(list) {
// return list[Math.floor(Math.random() * list.length)]
// }
function cloudmusic_dll_encode_id(some_id) {
let xoredString = ''
for (let i = 0; i < some_id.length; i++) {
@@ -26,7 +23,8 @@ function cloudmusic_dll_encode_id(some_id) {
}
module.exports = async (query, request) => {
const deviceId = getRandomFromList(deviceidList)
const deviceId = generateDeviceId()
logger.info(`Successfully registered anonimous token, deviceId: ${deviceId}`)
global.deviceId = deviceId
const encodedId = CryptoJS.enc.Base64.stringify(
CryptoJS.enc.Utf8.parse(
+2 -1
View File
@@ -9,6 +9,7 @@ module.exports = (query, request) => {
password: CryptoJS.MD5(query.password).toString(),
nickname: query.nickname,
countrycode: query.countrycode || '86',
force: 'false',
}
return request(`/api/register/cellphone`, data, createOption(query))
return request(`/api/w/register/cellphone`, data, createOption(query))
}
+11
View File
@@ -0,0 +1,11 @@
// 助眠解压 - 获取标签下资源列表
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
tag: query.tag,
firstQuery: false,
}
return request(`/api/voice/sati/resource/list`, data, createOption(query))
}
+13
View File
@@ -0,0 +1,13 @@
// 助眠解压 - 查看同类推荐
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
id: query.id,
}
return request(
`/api/voice/sati/resource/list/more/v1`,
data,
createOption(query),
)
}
+10
View File
@@ -0,0 +1,10 @@
// 助眠解压 - 收藏
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
id: query.id,
cancel: query.cancel || false,
}
return request(`/api/voice/sati/resource/sub`, data, createOption(query))
}
+7
View File
@@ -0,0 +1,7 @@
// 助眠解压 - 收藏列表
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {}
return request(`/api/voice/sati/resource/sub/list`, data, createOption(query))
}
+7
View File
@@ -0,0 +1,7 @@
// 助眠解压 - 标签列表
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {}
return request(`/api/voice/sati/tag/list`, data, createOption(query))
}
+13
View File
@@ -0,0 +1,13 @@
// 助眠解压 - 特定时间场景下的推荐资源
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
firstQuery: false,
}
return request(
`/api/voice/sati/timescene/resources/get`,
data,
createOption(query),
)
}
+13
View File
@@ -0,0 +1,13 @@
// 搜索建议pc端
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
keyword: query.keyword || '',
}
return request(
`/api/search/pc/suggest/keyword/get`,
data,
createOption(query),
)
}
+9
View File
@@ -0,0 +1,9 @@
// 灰色歌曲的其他版本推荐
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
songid: query.songid || query.id,
}
return request(`/api/song/copyright/rcmd`, data, createOption(query, 'eapi'))
}
+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/song/creators`, data, createOption(query))
}
+12
View File
@@ -0,0 +1,12 @@
// 喜欢歌曲
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const like = query.like !== 'false'
const data = {
trackId: query.id,
userid: query.uid,
like: like,
}
return request(`/api/song/like`, data, createOption(query))
}
+30 -27
View File
@@ -5,31 +5,34 @@ const createOption = require('../util/option.js')
const logger = require('../util/logger.js')
module.exports = async (query, request) => {
try {
const match = require("@unblockneteasemusic/server")
const source = query.source
? query.source.split(',') : ['pyncmd', 'kuwo', 'qq', 'migu', 'kugou']
const server = query.server ? query.server.split(',') : query.server
const result = await match(query.id, !server? source : server)
const proxy = process.env.PROXY_URL;
logger.info("开始解灰", query.id, result)
const useProxy = process.env.ENABLE_PROXY || "false"
if (result.url.includes('kuwo')) { result.proxyUrl = useProxy === 'true' ? proxy + result.url : result.url }
return {
status: 200,
body: {
code: 200,
data: result,
},
}
} catch (e) {
return {
status: 500,
body: {
code: 500,
msg: e.message || 'unblock error',
data: [],
},
}
try {
const {
matchID,
} = require('@neteasecloudmusicapienhanced/unblockmusic-utils')
const result = await matchID(query.id, query.source)
const proxy = process.env.PROXY_URL
logger.info('开始解灰', query.id, result)
const useProxy = process.env.ENABLE_PROXY || 'false'
if (result.data.url && result.data.url.includes('kuwo')) {
result.proxyUrl =
useProxy === 'true' ? proxy + result.data.url : result.data.url
}
}
return {
status: 200,
body: {
code: 200,
data: result.data.url,
proxyUrl: result.proxyUrl || '',
},
}
} catch (e) {
return {
status: 500,
body: {
code: 500,
msg: e.message || 'unblock error',
data: [],
},
}
}
}
+2 -72
View File
@@ -1,75 +1,5 @@
// GD音乐台get(适配SPlayer的UNM-Server)
// 感谢来自GD Studio的开发API
// https://music.gdstudio.xyz/
const createOption = require('../util/option.js')
// 夹带私货的东西就不要放在这里了
module.exports = async (query, request) => {
try {
const { id, br = "320" } = query;
if (!id) {
return {
status: 400,
body: {
code: 400,
message: "缺少必要参数 id",
data: [],
},
};
}
const validBR = ["128", "192", "320", "740", "999"];
// const covertBR = ["128000", "192000", "320000","740000", "999000"];
if (!validBR.includes(br)) {
return {
status: 400,
body: {
code: 400,
message: "无效音质参数",
allowed_values: validBR,
data: [],
},
};
}
const apiUrl = new URL("https://music-api.gdstudio.xyz/api.php");
apiUrl.searchParams.append("types", "url");
apiUrl.searchParams.append("id", id);
apiUrl.searchParams.append("br", br);
const response = await fetch(apiUrl.toString());
if (!response.ok) throw new Error(`API 响应状态: ${response.status}`);
const result = await response.json();
// 代理逻辑
const useProxy = process.env.ENABLE_PROXY || false;
const proxy = process.env.PROXY_URL;
if (useProxy && result.url && result.url.includes("kuwo")) {
result.proxyUrl = proxy + result.url.replace(/^http:\/\//, "http/");
}
return {
status: 200,
body: {
code: 200,
message: "请求成功",
data: {
id,
br,
url: result.url,
...(proxy && result.proxyUrl ? { proxyUrl: result.proxyUrl } : {})
}
}
};
} catch (error) {
console.error("Error in song_url_ncmget:", error);
return {
status: 500,
body: {
code: 500,
message: "服务器处理请求失败",
...(process.env.NODE_ENV === "development" ? { error: error.message } : {}),
data: [],
},
};
}
return { status: 200, body: { code: 200, data: [] } }
}
-35
View File
@@ -1,35 +0,0 @@
// 网易云歌曲解灰
// 支持qq音乐、酷狗音乐、酷我音乐、咪咕音乐、第三方网易云API等等(来自GD音乐台)
const createOption = require('../util/option.js')
const logger = require('../util/logger.js')
module.exports = async (query, request) => {
try {
const match = require("@unblockneteasemusic/server")
const source = query.source
? query.source.split(',') : ['pyncmd', 'kuwo', 'qq', 'migu', 'kugou']
const server = query.server ? query.server.split(',') : query.server
const result = await match(query.id, !server? source : server)
const proxy = process.env.PROXY_URL;
logger.info("开始解灰", query.id, result)
const useProxy = process.env.ENABLE_PROXY || "false"
if (result.url.includes('kuwo') && useProxy === "true") { result.proxyUrl = proxy + result.url }
return {
status: 200,
body: {
code: 200,
data: result,
},
}
} catch (e) {
return {
status: 500,
body: {
code: 500,
msg: e.message || 'unblock error',
data: [],
},
}
}
}
+32 -29
View File
@@ -1,13 +1,14 @@
// 歌曲链接 - v1
// 此版本不再采用 br 作为音质区分的标准
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
// 当unblock为true时, 会尝试使用unblockneteasemusic进行解锁, 同时音质设置不会生效, 但仍然为必须传入参数
// 当unblock为true时, 会尝试使用unblockmusic-utils进行解锁, 同时音质设置不会生效, 但仍然为必须传入参数
const logger = require('../util/logger.js')
const createOption = require('../util/option.js')
module.exports = async (query, request) => {
const match = require('@unblockneteasemusic/server')
const source = ['pyncmd','kuwo', 'qq', 'migu', 'kugou']
const {
matchID,
} = require('@neteasecloudmusicapienhanced/unblockmusic-utils')
require('dotenv').config()
const data = {
ids: '[' + query.id + ']',
@@ -16,35 +17,37 @@ module.exports = async (query, request) => {
}
if (query.unblock === 'true') {
try {
const result = await match(query.id, source)
logger.info('开始解灰', query.id, result)
if (result.url.includes('kuwo')) {
const useProxy = process.env.ENABLE_PROXY || 'false'
var proxyUrl = useProxy === 'true' ? process.env.PROXY_URL + result.url : result.url
const result = await matchID(query.id, query.source)
logger.info('Starting unblock(uses modules unblock):', query.id, result)
const useProxy = process.env.ENABLE_PROXY || 'false'
let proxyUrl = ''
if (result.data.url && result.data.url.includes('kuwo')) {
proxyUrl =
useProxy === 'true' && process.env.PROXY_URL
? process.env.PROXY_URL + result.data.url
: result.data.url
}
let url = Array.isArray(result) ? (result[0]?.url || result[0]) : (result.url || result)
if (url) {
return {
status: 200,
body: {
code: 200,
data: [
{
id: Number(query.id),
url,
type: 'flac',
level: query.level,
freeTrialInfo: 'null',
fee: 0,
proxyUrl: proxyUrl || '',
},
],
},
cookie: [],
}
return {
status: 200,
body: {
code: 200,
msg: 'Warning: Customizing unblock sources is not supported on this endpoint. Please use `/song/url/match` instead.',
data: [
{
id: Number(query.id),
url: result.data.url,
type: 'flac',
level: query.level,
freeTrialInfo: 'null',
fee: 0,
proxyUrl: proxyUrl || '',
},
],
},
cookie: [],
}
} catch (e) {
console.error('Error in unblockneteasemusic:', e)
console.error('Error in unblocking music:', e)
}
}
if (data.level == 'sky') {
+53
View File
@@ -0,0 +1,53 @@
// 获取客户端歌曲下载链接 - v1
// 此版本不再采用 br 作为音质区分的标准
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
const createOption = require('../util/option.js')
module.exports = async (query, request) => {
const data = {
id: query.id,
immerseType: 'c51',
level: query.level,
}
const response = await request(
`/api/song/enhance/download/url/v1`,
data,
createOption(query),
)
let url = response?.body?.data?.[0]?.url
if (!url) {
const fallbackData = {
ids: `[${query.id}]`,
level: query.level,
encodeType: 'flac',
}
if (query.level === 'sky') {
fallbackData.immerseType = 'c51'
}
const fallback = await request(
`/api/song/enhance/player/url/v1`,
fallbackData,
createOption(query),
)
url = fallback?.body?.data?.[0]?.url
if (!url) {
return fallback
}
return {
status: 302,
body: '',
cookie: fallback.cookie || [],
redirectUrl: url,
}
}
return {
status: 302,
body: '',
cookie: response.cookie || [],
redirectUrl: url,
}
}
+6
View File
@@ -0,0 +1,6 @@
// 所有榜单内容摘要v2
const createOption = require('../util/option.js')
module.exports = (query, request) => {
return request(`/api/toplist/detail/v2`, {}, createOption(query, 'weapi'))
}
+20
View File
@@ -0,0 +1,20 @@
// 用户详情
const createOption = require('../util/option.js')
module.exports = async (query, request) => {
const data = {
all: 'true',
userId: query.uid,
}
const res = await request(
`/api/w/v1/user/detail/${query.uid}`,
data,
createOption(query, 'eapi'),
)
// const result = JSON.stringify(res).replace(
// /avatarImgId_str/g,
// "avatarImgIdStr"
// );
// return JSON.parse(result);
return res
}
+1 -1
View File
@@ -5,7 +5,7 @@ module.exports = (query, request) => {
const data = {
userId: query.uid,
time: '0',
limit: query.limit || 30,
limit: query.limit || 20,
offset: query.offset || 0,
getcounts: 'true',
}
+14
View File
@@ -0,0 +1,14 @@
// 获取用户的收藏歌单列表
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
limit: query.limit || '100',
offset: query.offset || '0',
userId: query.uid,
isWebview: 'true',
includeRedHeart: 'true',
includeTop: 'true',
}
return request(`/api/user/playlist/collect`, data, createOption(query))
}
+14
View File
@@ -0,0 +1,14 @@
// 获取用户的创建歌单列表
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
limit: query.limit || '100',
offset: query.offset || '0',
userId: query.uid,
isWebview: 'true',
includeRedHeart: 'true',
includeTop: 'true',
}
return request(`/api/user/playlist/create`, data, createOption(query))
}
+7
View File
@@ -0,0 +1,7 @@
// 黑胶乐签打卡
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {}
return request(`/api/vip-center-bff/task/sign`, data, createOption(query))
}
+11
View File
@@ -0,0 +1,11 @@
// 黑胶乐签签到信息
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {}
return request(
`/api/vipnewcenter/app/user/sign/info`,
data,
createOption(query, 'weapi'),
)
}
+36 -22
View File
@@ -1,25 +1,26 @@
const { default: axios } = require('axios')
const fs = require('fs')
var xml2js = require('xml2js')
const createOption = require('../util/option.js')
var parser = new xml2js.Parser(/* options */)
const { getFileExtension, readFileChunk } = require('../util/fileHelper')
var parser = new xml2js.Parser()
function createDupkey() {
// 格式:3b443c7c-a87f-468d-ba38-46d407aaf23a
var s = []
var hexDigits = '0123456789abcdef'
for (var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1)
}
s[14] = '4' // bits 12-15 of the time_hi_and_version field to 0010
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) // bits 6-7 of the clock_seq_hi_and_reserved to 01
s[14] = '4'
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1)
s[8] = s[13] = s[18] = s[23] = '-'
return s.join('')
}
module.exports = async (query, request) => {
let ext = 'mp3'
if (query.songFile.name.indexOf('flac') > -1) {
ext = 'flac'
}
const ext = getFileExtension(query.songFile.name)
const filename =
query.songName ||
query.songFile.name
@@ -50,43 +51,58 @@ module.exports = async (query, request) => {
createOption(query, 'weapi'),
)
const objectKey = tokenRes.body.result.objectKey.replace('/', '%2F')
const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F')
const docId = tokenRes.body.result.docId
const res = await axios({
method: 'post',
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploads`,
headers: {
'x-nos-token': tokenRes.body.result.token,
'X-Nos-Meta-Content-Type': 'audio/mpeg',
'X-Nos-Meta-Content-Type': query.songFile.mimetype || 'audio/mpeg',
},
data: null,
})
// return xml
const res2 = await parser.parseStringPromise(res.data)
const fileSize = query.songFile.data.length
const blockSize = 10 * 1024 * 1024 // 10MB
const useTempFile = !!query.songFile.tempFilePath
let fileSize = query.songFile.size
if (useTempFile) {
const stats = await fs.promises.stat(query.songFile.tempFilePath)
fileSize = stats.size
}
const blockSize = 10 * 1024 * 1024
let offset = 0
let blockIndex = 1
let etags = []
while (offset < fileSize) {
const chunk = query.songFile.data.slice(
offset,
Math.min(offset + blockSize, fileSize),
)
let chunk
if (useTempFile) {
chunk = await readFileChunk(
query.songFile.tempFilePath,
offset,
Math.min(blockSize, fileSize - offset),
)
} else {
chunk = query.songFile.data.slice(
offset,
Math.min(offset + blockSize, fileSize),
)
}
const res3 = await axios({
method: 'put',
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?partNumber=${blockIndex}&uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
headers: {
'x-nos-token': tokenRes.body.result.token,
'Content-Type': 'audio/mpeg',
'Content-Type': query.songFile.mimetype || 'audio/mpeg',
},
data: chunk,
})
// get etag
const etag = res3.headers.etag
etags.push(etag)
offset += blockSize
@@ -101,19 +117,17 @@ module.exports = async (query, request) => {
}
completeStr += '</CompleteMultipartUpload>'
// 文件处理
await axios({
method: 'post',
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
headers: {
'Content-Type': 'text/plain;charset=UTF-8',
'X-Nos-Meta-Content-Type': 'audio/mpeg',
'X-Nos-Meta-Content-Type': query.songFile.mimetype || 'audio/mpeg',
'x-nos-token': tokenRes.body.result.token,
},
data: completeStr,
})
// preCheck
await request(
`/api/voice/workbench/voice/batch/upload/preCheck`,
{
+13
View File
@@ -0,0 +1,13 @@
// 我创建的播客声音
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
limit: query.limit || 20,
}
return request(
`/api/social/my/created/voicelist/v1`,
data,
createOption(query, 'weapi'),
)
}
+6 -9
View File
@@ -1,14 +1,11 @@
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
fee: '-1',
limit: query.limit || '200',
offset: query.offset || '0',
podcastName: query.podcastName || '',
keyword: query.keyword || '',
scene: 'normal',
limit: query.limit || '10',
offset: query.offset || '30',
e_r: true,
}
return request(
`/api/voice/workbench/voicelist/search`,
data,
createOption(query),
)
return request(`/api/search/voicelist/get`, data, createOption(query))
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { banner, lyric } from '@neteaseapireborn/api'
import { banner, lyric } from '@neteasecloudmusicapienhanced/api'
import { logger } from '../util/logger.js'
banner({ type: 0 }).then((res) => {
logger.info(res)
-11051
View File
File diff suppressed because it is too large Load Diff
+41 -37
View File
@@ -1,16 +1,17 @@
{
"name": "@neteaseapireborn/api",
"version": "4.28.29",
"description": "为停更的网易云音乐 NodeJs API 提供持续的维护!",
"name": "@neteasecloudmusicapienhanced/api",
"version": "4.32.1",
"description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护",
"scripts": {
"dev": "nodemon app.js",
"start": "node app.js",
"test": "mocha -r intelli-espower-loader -t 60000 server.test.js main.test.js --exit",
"lint": "eslint \"**/*.{js,ts}\"",
"lint-fix": "eslint --fix \"**/*.{js,ts}\"",
"prepare": "husky install",
"pkgwin": "pkg . -t node18-win-x64 -C GZip -o bin/app --no-bytecode",
"pkglinux": "pkg . -t node18-linux-x64 -C GZip -o bin/app --no-bytecode",
"pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o bin/app --no-bytecode"
"pkgwin": "pkg . -t node18-win-x64 -C GZip -o precompiled/app",
"pkglinux": "pkg . -t node18-linux-x64 -C GZip -o precompiled/app",
"pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o precompiled/app"
},
"bin": "./app.js",
"pkg": {
@@ -19,7 +20,6 @@
"node_modules/axios",
"node_modules/express",
"node_modules/express-fileupload",
"node_modules/md5",
"node_modules/music-metadata",
"node_modules/pac-proxy-agent",
"node_modules/qrcode",
@@ -50,7 +50,7 @@
"git add"
]
},
"author": "binaryify",
"author": "MoeFurina",
"license": "MIT",
"files": [
"module",
@@ -65,40 +65,44 @@
"data"
],
"dependencies": {
"@unblockneteasemusic/server": "^0.27.10",
"axios": "^1.2.2",
"@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.3.1",
"axios": "^1.16.1",
"crypto-js": "^4.2.0",
"dotenv": "^16.0.3",
"express": "^4.17.1",
"express-fileupload": "^1.1.9",
"md5": "^2.3.0",
"music-metadata": "^7.5.3",
"node-forge": "^1.3.1",
"pac-proxy-agent": "^7.0.0",
"qrcode": "^1.4.4",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"express-fileupload": "^1.5.2",
"gzip": "^0.1.0",
"music-metadata": "^11.12.3",
"node-forge": "^1.4.0",
"pac-proxy-agent": "^7.2.0",
"qrcode": "^1.5.4",
"safe-decode-uri-component": "^1.2.1",
"tunnel": "^0.0.6",
"xml2js": "^0.6.2",
"yargs": "^17.1.1"
"yargs": "^18.0.0"
},
"devDependencies": {
"@types/express": "^4.17.13",
"@types/express-fileupload": "^1.2.2",
"@types/mocha": "^9.1.0",
"@types/node": "16.11.19",
"@typescript-eslint/eslint-plugin": "5.0.0",
"@typescript-eslint/parser": "5.0.0",
"eslint": "8.7.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-html": "7.1.0",
"eslint-plugin-prettier": "4.0.0",
"husky": "7.0.4",
"intelli-espower-loader": "1.1.0",
"lint-staged": "12.1.7",
"mocha": "10.0.0",
"pkg": "^5.8.0",
"power-assert": "1.6.1",
"prettier": "2.7.1",
"typescript": "4.5.2"
"@eslint/eslintrc": "^3.3.5",
"@eslint/js": "^9.39.4",
"@types/express": "^5.0.6",
"@types/express-fileupload": "^1.5.1",
"@types/mocha": "^10.0.10",
"@types/node": "25.5.0",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-html": "^8.1.4",
"eslint-plugin-prettier": "^5.5.5",
"globals": "^17.6.0",
"husky": "^9.1.7",
"intelli-espower-loader": "^1.1.0",
"lint-staged": "^16.4.0",
"mocha": "^11.7.5",
"nodemon": "^3.1.14",
"pkg": "^5.8.1",
"power-assert": "^1.6.1",
"prettier": "^3.8.3",
"typescript": "^5.9.3"
}
}
+67 -19
View File
@@ -1,20 +1,17 @@
const { default: axios } = require('axios')
const createOption = require('../util/option.js')
const logger = require('../util/logger.js')
const {
getUploadData,
getFileExtension,
sanitizeFilename,
} = require('../util/fileHelper')
module.exports = async (query, request) => {
let ext = 'mp3'
// if (query.songFile.name.indexOf('flac') > -1) {
// ext = 'flac'
// }
if (query.songFile.name.includes('.')) {
ext = query.songFile.name.split('.').pop()
}
const filename = query.songFile.name
.replace('.' + ext, '')
.replace(/\s/g, '')
.replace(/\./g, '_')
const ext = getFileExtension(query.songFile.name)
const filename = sanitizeFilename(query.songFile.name)
const bucket = 'jd-musicrep-privatecloud-audio-public'
// 获取key和token
const tokenRes = await request(
`/api/nos/token/alloc`,
{
@@ -29,31 +26,82 @@ module.exports = async (query, request) => {
createOption(query, 'weapi'),
)
// 上传
const objectKey = tokenRes.body.result.objectKey.replace('/', '%2F')
if (!tokenRes.body.result || !tokenRes.body.result.objectKey) {
logger.error('Token分配失败:', tokenRes.body)
throw {
status: 500,
body: {
code: 500,
msg: '获取上传token失败',
detail: tokenRes.body,
},
}
}
const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F')
let lbs
try {
const lbs = (
lbs = (
await axios({
method: 'get',
url: `https://wanproxy.127.net/lbs?version=1.0&bucketname=${bucket}`,
timeout: 10000,
})
).data
} catch (error) {
logger.error('LBS获取失败:', error.message)
throw {
status: 500,
body: {
code: 500,
msg: '获取上传服务器地址失败',
detail: error.message,
},
}
}
if (!lbs || !lbs.upload || !lbs.upload[0]) {
logger.error('无效的LBS响应:', lbs)
throw {
status: 500,
body: {
code: 500,
msg: '获取上传服务器地址无效',
detail: lbs,
},
}
}
try {
await axios({
method: 'post',
url: `${lbs.upload[0]}/${bucket}/${objectKey}?offset=0&complete=true&version=1.0`,
headers: {
'x-nos-token': tokenRes.body.result.token,
'Content-MD5': query.songFile.md5,
'Content-Type': 'audio/mpeg',
'Content-Type': query.songFile.mimetype || 'audio/mpeg',
'Content-Length': String(query.songFile.size),
},
data: query.songFile.data,
data: getUploadData(query.songFile),
maxContentLength: Infinity,
maxBodyLength: Infinity,
timeout: 300000,
})
logger.info('上传成功:', filename)
} catch (error) {
logger.info('error', error.response)
throw error.response
logger.error('上传失败:', {
status: error.response?.status,
data: error.response?.data,
message: error.message,
})
throw {
status: error.response?.status || 500,
body: {
code: error.response?.status || 500,
msg: '文件上传失败',
detail: error.response?.data || error.message,
},
}
}
return {
...tokenRes,
+5 -7
View File
@@ -1,5 +1,7 @@
const { default: axios } = require('axios')
const createOption = require('../util/option.js')
const { getUploadData } = require('../util/fileHelper')
module.exports = async (query, request) => {
const data = {
bucket: 'yyimgs',
@@ -10,27 +12,23 @@ module.exports = async (query, request) => {
return_body: `{"code":200,"size":"$(ObjectSize)"}`,
type: 'other',
}
// 获取key和token
const res = await request(
`/api/nos/token/alloc`,
data,
createOption(query, 'weapi'),
)
// 上传图片
const res2 = await axios({
method: 'post',
url: `https://nosup-hz1.127.net/yyimgs/${res.body.result.objectKey}?offset=0&complete=true&version=1.0`,
headers: {
'x-nos-token': res.body.result.token,
'Content-Type': 'image/jpeg',
'Content-Type': query.imgFile.mimetype || 'image/jpeg',
},
data: query.imgFile.data,
data: getUploadData(query.imgFile),
})
return {
// ...res.body.result,
// ...res2.data,
// ...res3.body,
url_pre: 'https://p1.music.126.net/' + res.body.result.objectKey,
imgId: res.body.result.docId,
}
+1474 -1095
View File
File diff suppressed because it is too large Load Diff
+144 -49
View File
@@ -5,82 +5,148 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 调试界面</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
margin: 20px;
display: flex;
flex-direction: column;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
min-height: 100vh;
background: #f5f5f5;
padding: 20px;
}
.container {
display: flex;
flex-direction: column;
flex-grow: 1;
max-width: 1200px;
margin: 0 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: 24px;
}
form {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
margin-bottom: 10px;
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 24px;
}
input, button {
padding: 10px;
box-sizing: border-box;
flex: 1;
.form-row {
display: flex;
gap: 12px;
align-items: center;
}
label {
font-size: 14px;
font-weight: 500;
color: #555;
min-width: 80px;
}
input, select {
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
flex: 1;
outline: none;
}
input:focus, select:focus {
border-color: #333;
}
button {
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
background: #333;
color: white;
padding: 10px 24px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s ease;
}
button:hover {
background: #555;
}
.data-result {
display: flex;
flex-direction: row;
flex-grow: 1;
display: flex;
gap: 16px;
min-height: 400px;
}
.data-result > div {
display: flex;
flex-direction: column;
flex-grow: 1;
padding: 10px;
box-sizing: border-box;
flex: 1;
display: flex;
flex-direction: column;
}
.data-result label {
margin-bottom: 10px;
margin-bottom: 8px;
padding: 0;
}
#data, #result {
height: 100%;
box-sizing: border-box;
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;
}
#data {
border-right: 1px solid #ccc;
textarea:focus {
border-color: #333;
}
</style>
</head>
<body>
<div class="container">
<h1>API 调试界面</h1>
<form onsubmit="event.preventDefault(); sendRequest();">
<label for="uri">uri</label>
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
<label for="crypto">crypto</label>
<select id="crypto" name="crypto">
<option value="weapi">weapi</option>
<option value="eapi">eapi</option>
<option value="api">api</option>
<option value="linuxapi">linuxapi</option>
<option value="" selected>(默认)</option>
</select>
<button type="submit">发送</button>
<div class="form-row">
<label for="uri">URI</label>
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
</div>
<div class="form-row">
<label for="crypto">加密方式</label>
<select id="crypto" name="crypto">
<option value="weapi">weapi</option>
<option value="eapi">eapi</option>
<option value="api">api</option>
<option value="linuxapi">linuxapi</option>
<option value="" selected>(默认)</option>
</select>
</div>
<div class="form-row">
<label></label>
<button type="submit">发送请求</button>
</div>
</form>
<div class="data-result">
<div>
<label for="result">result</label>
<textarea id="result" name="result"></textarea>
<label for="result">响应结果</label>
<textarea id="result" name="result" readonly></textarea>
</div>
<div>
<label for="data">data</label>
<label for="data">请求数据</label>
<textarea id="data" name="data">
{
"cp": false,
@@ -122,6 +188,35 @@
document.getElementById('result').value = 'Request failed: ' + error.message;
}
}
(function fillFromQuery() {
const params = new URLSearchParams(window.location.search);
if (!params.toString()) return;
const uri = params.get('uri');
const crypto = params.get('crypto');
const dataParam = params.get('data');
if (uri) {
document.getElementById('uri').value = uri;
}
if (crypto) {
const cryptoSelect = document.getElementById('crypto');
if ([...cryptoSelect.options].some((opt) => opt.value === crypto)) {
cryptoSelect.value = crypto;
}
}
if (dataParam) {
const decoded = dataParam;
try {
document.getElementById('data').value = JSON.stringify(
JSON.parse(decoded),
null,
2,
);
} catch (error) {
document.getElementById('data').value = decoded;
}
}
})();
</script>
</body>
</html>
+2 -3
View File
@@ -1,6 +1,5 @@
'use strict'
const WASM_BINARY_PLACEHOLDER = 'WASM_BINARY_PLACEHOLDER';
const logger = require('../../util/logger.js')
// See https://github.com/Distributive-Network/PythonMonkey/issues/266
if (typeof globalThis.setInterval != 'function'){
globalThis.setInterval = function pm$$setInterval(fn, timeout) {
@@ -1612,9 +1611,9 @@ function instantiateRuntime(){
function GenerateFP(floatArray) {
let PCMBuffer = Float32Array.from(floatArray)
logger.info('[afp] input samples n=', PCMBuffer.length)
console.info('[afp] input samples n=', PCMBuffer.length)
return instantiateRuntime().then((fpRuntime) => {
logger.info('[afp] begin fingerprinting')
console.info('[afp] begin fingerprinting')
let fp_vector = fpRuntime.ExtractQueryFP(PCMBuffer.buffer)
let result_buf = new Uint8Array(fp_vector.size());
for (let t = 0; t < fp_vector.size(); t++)
+212 -43
View File
@@ -1,27 +1,142 @@
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>听歌识曲 Demo</title>
<style>
* {
font-family: sans-serif;
margin: 0;
padding: 0;
box-sizing: border-box;
}
pre {
font-family: monospace;
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
min-height: 100vh;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 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;
}
.subtitle {
font-size: 13px;
color: #666;
margin-bottom: 24px;
}
hr {
border: none;
border-top: 1px solid #eee;
margin: 20px 0;
}
p {
font-size: 14px;
color: #555;
line-height: 1.6;
margin-bottom: 12px;
}
a {
font-family: sans-serif;
color: #333;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.section {
margin-bottom: 24px;
}
.section h3 {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.control-group {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 16px;
}
button {
padding: 10px 20px;
background: #333;
color: white;
font-size: 14px;
font-weight: 500;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
}
button:hover {
background: #555;
}
button:disabled {
background: #999;
cursor: not-allowed;
}
input[type="file"] {
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-group input[type="checkbox"] {
cursor: pointer;
}
.checkbox-group label {
margin: 0;
cursor: pointer;
font-size: 14px;
color: #555;
}
audio {
width: 100%;
margin-bottom: 16px;
}
canvas {
width: 100%;
height: 0;
transition: all linear 0.1s;
background: #f9f9f9;
border-radius: 6px;
}
.canvas-active {
@@ -29,39 +144,80 @@
}
pre {
overflow: scroll;
font-family: 'Courier New', monospace;
font-size: 13px;
color: #666;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 400px;
overflow-y: auto;
padding: 16px;
background: #f9f9f9;
border-radius: 6px;
border: 1px solid #eee;
}
.warning {
padding: 12px 16px;
background: #fef3c7;
border-radius: 6px;
font-size: 14px;
color: #92400e;
margin-bottom: 16px;
}
</style>
</head>
<body>
<h1>听歌识曲 Demo (Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a>)</h1>
<hr>
<p><b>DISCLAIMER: </b></p>
<p>This site uses the offical NetEase audio matcher APIs (reverse engineered from <a
href="https://fn.music.163.com/g/chrome-extension-home-page-beta/">https://fn.music.163.com/g/chrome-extension-home-page-beta/</a>)
</p>
<p>And DOES NOT condone copyright infringment nor intellectual property theft.</p>
<hr>
<p><b>NOTE:</b></p>
<p>Before start using the site, you may want to access this link first:</p>
<a href="https://cors-anywhere.herokuapp.com/corsdemo">https://cors-anywhere.herokuapp.com/corsdemo</a>
<p>Since Netease APIs do not have CORS headers, this is required to alleviate this restriction.</p>
<hr>
<p>Usage:</p>
<li>Select your audio file through "Choose File" picker</li>
<li>Hit the "Clip" button and wait for the results!</li>
<div class="container">
<h1>听歌识曲 Demo</h1>
<p class="subtitle">Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a></p>
<audio id="audio" controls autoplay></audio>
<canvas id="canvas"></canvas>
<button id="invoke">Clip</button>
<input type="file" name="picker" accept="*" id="file">
<hr>
<label for="use-mic">Mix in Microphone input</label>
<input type="checkbox" name="use-mic" id="usemic">
<hr>
<pre id="logs"></pre>
<hr>
<div class="warning">
<strong>免责声明:</strong>本站点使用网易云音乐官方音频识别API(逆向自 <a href="https://fn.music.163.com/g/chrome-extension-home-page-beta/" target="_blank">Chrome 扩展页面</a>),不鼓励版权侵犯或知识产权盗窃。
</div>
<div class="section">
<h3>使用说明</h3>
<p>在使用本站点之前,您可能需要先访问以下链接:</p>
<p><a href="https://cors-anywhere.herokuapp.com/corsdemo" target="_blank">https://cors-anywhere.herokuapp.com/corsdemo</a></p>
<p>由于网易云音乐API没有CORS头,这是解决此限制的必要步骤。</p>
</div>
<div class="section">
<h3>使用方法</h3>
<ul style="padding-left: 20px; font-size: 14px; color: #555;">
<li>通过"选择文件"选择您的音频文件</li>
<li>点击"识别"按钮并等待结果</li>
</ul>
</div>
<hr>
<audio id="audio" controls autoplay></audio>
<canvas id="canvas"></canvas>
<div class="control-group">
<button id="invoke">识别</button>
<input type="file" name="picker" accept="*" id="file">
</div>
<div class="control-group">
<div class="checkbox-group">
<input type="checkbox" name="use-mic" id="usemic">
<label for="usemic">混合麦克风输入</label>
</div>
</div>
<hr>
<h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 12px;">日志</h3>
<pre id="logs"></pre>
</div>
</body>
<script src="./afp.wasm.js"></script>
<script src="./afp.js"></script>
<script type="module">
@@ -76,13 +232,17 @@
let canvas = document.getElementById('canvas')
let canvasCtx = canvas.getContext('2d')
let logs = document.getElementById('logs')
logs.write = line => logs.innerHTML += line + '\n'
logs.write = line => {
// Append log lines as text to avoid interpreting content as HTML
logs.appendChild(document.createTextNode(line));
logs.appendChild(document.createElement('br'));
}
function RecorderCallback(channelL) {
let sampleBuffer = new Float32Array(channelL.subarray(0, duration * 8000))
GenerateFP(sampleBuffer).then(FP => {
logs.write(`[index] Generated FP ${FP}`)
logs.write('[index] Now querying, please wait...')
logs.write(`[index] 生成指纹 ${FP}`)
logs.write('[index] 正在查询,请稍候...')
fetch(
'/audio/match?' +
new URLSearchParams({
@@ -91,9 +251,9 @@
method: 'POST'
}).then(resp => resp.json()).then(resp => {
if (!resp.data.result) {
return logs.write('[index] Query failed with no results.')
return logs.write('[index] 查询失败,无结果')
}
logs.write(`[index] Query complete. Results=${resp.data.result.length}`)
logs.write(`[index] 查询完成。结果数量=${resp.data.result.length}`)
for (var song of resp.data.result) {
logs.write(
`[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>`
@@ -104,20 +264,19 @@
}
function InitAudioCtx() {
// AFP.wasm can't do it with anything other than 8KHz
audioCtx = new AudioContext({ 'sampleRate': 8000 })
if (audioCtx.state == 'suspended')
return false
let audioNode = audioCtx.createMediaElementSource(audio)
audioCtx.audioWorklet.addModule('rec.js').then(() => {
recorderNode = new AudioWorkletNode(audioCtx, 'timed-recorder')
audioNode.connect(recorderNode) // recorderNode doesn't output anything
audioNode.connect(recorderNode)
audioNode.connect(audioCtx.destination)
recorderNode.port.onmessage = event => {
switch (event.data.message) {
case 'finished':
RecorderCallback(event.data.recording)
clip.innerHTML = 'Clip'
clip.innerHTML = '识别'
clip.disabled = false
canvas.classList.remove('canvas-active')
break
@@ -130,7 +289,6 @@
logs.write(event.data.message)
}
}
// Attempt to get user's microphone and connect it to the AudioContext.
navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: false,
@@ -142,7 +300,7 @@
micSourceNode = audioCtx.createMediaStreamSource(micStream);
micSourceNode.connect(recorderNode)
usemic.checked = true
logs.write('[rec.js] Microphone attached.')
logs.write('[rec.js] 麦克风已连接')
});
});
return true
@@ -161,10 +319,20 @@
else
micSourceNode.connect(recorderNode)
})
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/\//g, '&#x2F;');
}
file.addEventListener('change', event => {
file.files[0].arrayBuffer().then(
async buffer => {
logs.write(`[index] File ${file.files[0].name} loaded.`)
const safeName = escapeHtml(file.files[0].name)
logs.write(`[index] 文件 ${safeName} 已加载`)
audio.src = window.URL.createObjectURL(new Blob([buffer]))
clip.disabled = false
})
@@ -188,12 +356,13 @@
UpdateCanvas()
let requestCtx = setInterval(() => {
try {
if (InitAudioCtx()) { // Put this here so we don't have to deal with the 'user did not interact' thing
if (InitAudioCtx()) {
clearInterval(requestCtx)
logs.write('[rec.js] Audio Context started.')
logs.write('[rec.js] 音频上下文已启动')
}
} catch {
// Fail silently
}
}, 100)
</script>
</html>
+296 -43
View File
@@ -5,67 +5,320 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>更新头像</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<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;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
max-width: 400px;
width: 100%;
text-align: center;
}
h1 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
color: #666;
margin-bottom: 32px;
}
.avatar-wrapper {
position: relative;
width: 160px;
height: 160px;
margin: 0 auto 32px;
}
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
border: 3px solid #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.avatar-wrapper.loading .avatar {
opacity: 0.5;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.avatar-wrapper.loading .loading-overlay {
opacity: 1;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #e0e0e0;
border-top-color: #333;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.upload-btn {
display: inline-block;
position: relative;
padding: 12px 28px;
background: #333;
color: white;
font-size: 15px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
border: none;
}
.upload-btn:hover {
background: #555;
}
.upload-btn input[type="file"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.login-link {
display: block;
margin-top: 24px;
color: #666;
font-size: 14px;
text-decoration: none;
}
.login-link:hover {
color: #333;
text-decoration: underline;
}
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(100px);
padding: 12px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
opacity: 0;
transition: all 0.3s ease;
z-index: 1000;
max-width: 90%;
text-align: center;
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
.toast.success {
background: #10b981;
}
.toast.error {
background: #ef4444;
}
.toast.info {
background: #3b82f6;
}
.hint {
margin-top: 20px;
font-size: 12px;
color: #999;
}
</style>
</head>
<body>
<div>
<a href="/qrlogin-nocookie.html">
如果没登录,请先登录
<div class="container">
<h1>更新头像</h1>
<p class="subtitle">选择一张图片作为您的新头像</p>
<div class="avatar-wrapper" id="avatarWrapper">
<img id="avatar" class="avatar" src="" alt="头像" />
<div class="loading-overlay">
<div class="spinner"></div>
</div>
</div>
<label class="upload-btn">
选择图片
<input id="file" type="file" accept="image/*" />
</label>
<a href="/qrlogin-nocookie.html" class="login-link">
还没有登录?点击登录
</a>
<p class="hint">支持 JPG、PNG 格式,建议尺寸 200x200</p>
</div>
<input id="file" type="file" />
<img id="avatar" style="height: 200px; width: 200px; border-radius: 50%" />
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js
"></script>
<div id="toast" class="toast"></div>
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
<script>
main()
async function main() {
document.querySelector('input[type="file"]').addEventListener(
'change',
function (e) {
var file = this.files[0]
upload(file)
},
false,
)
const res = await axios({
url: `/user/detail?uid=32953014&timestamp=${Date.now()}`,
withCredentials: true, //跨域的话必须设置
})
document.querySelector('#avatar').src = res.data.profile.avatarUrl
const fileInput = document.querySelector('input[type="file"]');
const avatarWrapper = document.getElementById('avatarWrapper');
fileInput.addEventListener('change', function(e) {
const file = this.files[0];
if (file) {
upload(file);
}
}, false);
try {
showToast('正在加载头像...', 'info');
avatarWrapper.classList.add('loading');
const res = await axios({
url: `/user/detail?uid=32953014&timestamp=${Date.now()}`,
withCredentials: true
});
document.querySelector('#avatar').src = res.data.profile.avatarUrl;
hideToast();
} catch (error) {
hideToast();
showToast('加载头像失败,请刷新页面重试', 'error');
console.error('加载头像失败:', error);
} finally {
avatarWrapper.classList.remove('loading');
}
}
async function upload(file) {
var formData = new FormData()
formData.append('imgFile', file)
const imgSize = await getImgSize(file)
const res = await axios({
method: 'post',
url: `/avatar/upload?cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width
}&imgX=0&imgY=0&timestamp=${Date.now()}`,
headers: {
'Content-Type': 'multipart/form-data',
},
data: formData,
})
document.querySelector('#avatar').src = res.data.data.url
const avatarWrapper = document.getElementById('avatarWrapper');
if (!file.type.startsWith('image/')) {
showToast('请选择图片文件', 'error');
return;
}
try {
showToast('正在上传头像...', 'info');
avatarWrapper.classList.add('loading');
var formData = new FormData();
formData.append('imgFile', file);
const imgSize = await getImgSize(file);
const res = await axios({
method: 'post',
url: `/avatar/upload?cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width}&imgX=0&imgY=0&timestamp=${Date.now()}`,
headers: {
'Content-Type': 'multipart/form-data',
},
data: formData,
});
document.querySelector('#avatar').src = res.data.data.url;
showToast('头像更新成功!', 'success');
} catch (error) {
console.error('上传失败:', error);
const errorMsg = error.response?.data?.message || error.message || '上传失败,请重试';
showToast(errorMsg, 'error');
} finally {
avatarWrapper.classList.remove('loading');
}
}
function getImgSize(file) {
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = function (theFile) {
let image = new Image()
image.src = theFile.target.result
image.onload = function () {
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function(theFile) {
let image = new Image();
image.src = theFile.target.result;
image.onload = function() {
resolve({
width: this.width,
height: this.height,
})
}
}
})
});
};
image.onerror = function() {
reject(new Error('图片加载失败'));
};
};
reader.onerror = function() {
reject(new Error('文件读取失败'));
};
});
}
function showToast(message, type = 'info') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast ${type}`;
requestAnimationFrame(() => {
toast.classList.add('show');
});
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
function hideToast() {
const toast = document.getElementById('toast');
toast.classList.remove('show');
}
</script>
</body>
+536 -62
View File
@@ -4,27 +4,285 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>云盘上传</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: 800px;
margin: 0 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: 24px;
}
.login-link {
display: block;
margin-bottom: 24px;
color: #666;
font-size: 14px;
text-decoration: none;
}
.login-link:hover {
color: #333;
text-decoration: underline;
}
.mode-section {
margin-bottom: 24px;
padding: 16px;
background: #f9f9f9;
border-radius: 8px;
}
.mode-section label {
display: block;
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 12px;
}
.mode-options {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.mode-option {
display: flex;
align-items: flex-start;
gap: 8px;
cursor: pointer;
}
.mode-option input[type="radio"] {
margin-top: 3px;
}
.mode-option-text {
display: flex;
flex-direction: column;
}
.mode-option-title {
font-size: 14px;
color: #333;
}
.mode-option-desc {
font-size: 12px;
color: #999;
margin-top: 2px;
}
.mode-option input[type="radio"]:checked + .mode-option-text .mode-option-title {
color: #333;
font-weight: 500;
}
.upload-section {
margin-bottom: 32px;
}
.upload-btn {
display: inline-block;
padding: 12px 28px;
background: #333;
color: white;
font-size: 15px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
border: none;
}
.upload-btn:hover {
background: #555;
}
.upload-btn input[type="file"] {
display: none;
}
.upload-btn.disabled {
background: #ccc;
cursor: not-allowed;
}
.songs-list {
list-style: none;
}
.song-item {
padding: 12px 16px;
border-bottom: 1px solid #eee;
font-size: 14px;
color: #333;
}
.song-item:last-child {
border-bottom: none;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #999;
font-size: 14px;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
.progress-section {
margin-bottom: 24px;
display: none;
}
.progress-section.active {
display: block;
}
.progress-item {
margin-bottom: 12px;
padding: 12px;
background: #f9f9f9;
border-radius: 6px;
}
.progress-item .name {
font-size: 14px;
color: #333;
margin-bottom: 8px;
word-break: break-all;
}
.progress-item .status {
font-size: 12px;
color: #666;
margin-bottom: 6px;
}
.progress-bar {
height: 6px;
background: #e0e0e0;
border-radius: 3px;
overflow: hidden;
}
.progress-bar .fill {
height: 100%;
background: #333;
border-radius: 3px;
transition: width 0.3s ease;
width: 0%;
}
.progress-item.success .fill {
background: #4caf50;
}
.progress-item.error .fill {
background: #f44336;
}
.progress-item.error .status {
color: #f44336;
}
.info-text {
font-size: 12px;
color: #999;
margin-top: 8px;
}
.warning-text {
font-size: 12px;
color: #e65100;
margin-top: 8px;
}
</style>
</head>
<body>
<div>
<a href="/qrlogin-nocookie.html"> 如果没登录,请先登录 </a>
</div>
<input id="file" type="file" multiple />
<div id="app">
<ul>
<li v-for="(item,index) in songs" :key="index">{{item.songName}}</li>
</ul>
<div class="container">
<h1>云盘上传</h1>
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
<div class="mode-section">
<label>上传模式</label>
<div class="mode-options">
<label class="mode-option">
<input type="radio" name="uploadMode" value="direct" checked />
<span class="mode-option-text">
<span class="mode-option-title">客户端直传</span>
<span class="mode-option-desc">文件直接上传到云存储,支持大文件,适合 Vercel 等平台</span>
</span>
</label>
<label class="mode-option">
<input type="radio" name="uploadMode" value="proxy" />
<span class="mode-option-text">
<span class="mode-option-title">后端代理</span>
<span class="mode-option-desc">文件通过服务器转发,更简洁,需要服务器支持大文件</span>
</span>
</label>
</div>
</div>
<div class="upload-section">
<label class="upload-btn" id="uploadBtn">
选择文件(支持多选)
<input id="file" type="file" multiple accept="audio/*" />
</label>
<p class="info-text" id="modeInfo">支持大文件上传,文件将直接传输到云存储服务器</p>
</div>
<div id="progressSection" class="progress-section"></div>
<div id="app">
<div v-if="loading" class="loading">加载中...</div>
<ul v-else-if="songs.length > 0" class="songs-list">
<li v-for="(item, index) in songs" :key="index" class="song-item">
{{ item.songName }}
</li>
</ul>
<div v-else class="empty-state">暂无云盘歌曲</div>
</div>
</div>
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
<script src="https://fastly.jsdelivr.net/npm/vue"></script>
<script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsmediatags/3.9.5/jsmediatags.min.js"></script>
<script>
const logger = require('../util/logger.js')
const app = Vue.createApp({
data() {
return {
songs: [],
loading: false,
}
},
created() {
@@ -32,73 +290,289 @@
},
methods: {
getData() {
logger.info('getdata')
const _this = this
this.loading = true
axios({
url: `/user/cloud?time=${Date.now()}&cookie=${localStorage.getItem(
'cookie',
)}`,
}).then((res) => {
logger.info(res.data)
_this.songs = res.data.data
url: `/user/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
})
.then((res) => {
this.songs = res.data.data || []
})
.catch((err) => {
console.error('获取云盘数据失败:', err)
})
.finally(() => {
this.loading = false
})
},
},
}).mount('#app')
const fileUpdateTime = {}
let fileLength = 0
let isUploading = false
let uploadMode = 'direct'
const progressSection = document.getElementById('progressSection')
const uploadBtn = document.getElementById('uploadBtn')
const fileInput = document.querySelector('input[type="file"]')
const modeInfo = document.getElementById('modeInfo')
document.querySelectorAll('input[name="uploadMode"]').forEach(radio => {
radio.addEventListener('change', function() {
uploadMode = this.value
if (uploadMode === 'direct') {
modeInfo.textContent = '支持大文件上传,文件将直接传输到云存储服务器'
modeInfo.className = 'info-text'
} else {
modeInfo.textContent = '文件将通过服务器转发,服务器需支持大文件上传(Vercel 限制 4.5MB)'
modeInfo.className = 'warning-text'
}
})
})
function main() {
document
.querySelector('input[type="file"]')
.addEventListener('change', function (e) {
logger.info(this.files)
let currentIndx = 0
fileLength = this.files.length
for (const item of this.files) {
currentIndx += 1
upload(item, currentIndx)
}
})
fileInput.addEventListener('change', function (e) {
const files = this.files
if (files.length === 0) return
if (isUploading) return
uploadFilesSequentially(Array.from(files))
this.value = ''
})
}
main()
function upload(file, currentIndx) {
var formData = new FormData()
formData.append('songFile', file)
axios({
method: 'post',
url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem(
'cookie',
)}`,
headers: {
'Content-Type': 'multipart/form-data',
},
data: formData,
})
.then((res) => {
logger.info(`${file.name} 上传成功`)
if (currentIndx >= fileLength) {
logger.info('上传完毕')
}
app.getData()
async function uploadFilesSequentially(files) {
isUploading = true
uploadBtn.classList.add('disabled')
progressSection.classList.add('active')
progressSection.innerHTML = ''
for (let i = 0; i < files.length; i++) {
if (uploadMode === 'direct') {
await uploadFileDirect(files[i], i + 1, files.length)
} else {
await uploadFileProxy(files[i], i + 1, files.length)
}
}
isUploading = false
uploadBtn.classList.remove('disabled')
app.getData()
}
function createProgressItem(file, index, total) {
const item = document.createElement('div')
item.className = 'progress-item'
item.id = `progress-${index}`
item.innerHTML = `
<div class="name">${file.name} (${formatSize(file.size)})</div>
<div class="status">准备中...</div>
<div class="progress-bar"><div class="fill"></div></div>
`
progressSection.appendChild(item)
return item
}
function updateProgress(index, status, percent, isError = false) {
const item = document.getElementById(`progress-${index}`)
if (!item) return
item.querySelector('.status').textContent = status
item.querySelector('.fill').style.width = `${percent}%`
if (isError) {
item.classList.add('error')
} else if (percent >= 100) {
item.classList.add('success')
}
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}
async function uploadFileProxy(file, index, total) {
createProgressItem(file, index, total)
try {
updateProgress(index, '上传中...', 10)
const formData = new FormData()
formData.append('songFile', file)
await axios({
method: 'post',
url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
headers: {
'Content-Type': 'multipart/form-data',
},
data: formData,
onUploadProgress: (progressEvent) => {
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 90) + 10
updateProgress(index, `上传中... ${Math.round(progressEvent.loaded / progressEvent.total * 100)}%`, Math.min(percent, 100))
},
timeout: 600000,
})
.catch(async (err) => {
logger.info(err)
logger.info(fileUpdateTime)
fileUpdateTime[file.name]
? (fileUpdateTime[file.name] += 1)
: (fileUpdateTime[file.name] = 1)
if (fileUpdateTime[file.name] >= 4) {
console.error(`丢,这首歌怎么都传不上:${file.name}`)
return
updateProgress(index, '上传完成!', 100)
} catch (err) {
console.error(`${file.name} 上传失败:`, err)
const errorMsg = err.response?.data?.msg || err.message || '未知错误'
if (err.response?.status === 413 || errorMsg.includes('PAYLOAD_TOO_LARGE')) {
updateProgress(index, '文件过大,请切换到客户端直传模式', 0, true)
} else {
updateProgress(index, `上传失败: ${errorMsg}`, 0, true)
}
}
}
async function calculateMD5(file) {
return new Promise((resolve, reject) => {
const chunkSize = 2 * 1024 * 1024
const chunks = Math.ceil(file.size / chunkSize)
let currentChunk = 0
const spark = new SparkMD5.ArrayBuffer()
const reader = new FileReader()
reader.onload = (e) => {
spark.append(e.target.result)
currentChunk++
if (currentChunk < chunks) {
loadNext()
} else {
console.error(`${file.name} 失败 ${fileUpdateTime[file.name]} 次`)
resolve(spark.end())
}
}
reader.onerror = () => reject(reader.error)
function loadNext() {
const start = currentChunk * chunkSize
const end = Math.min(start + chunkSize, file.size)
reader.readAsArrayBuffer(file.slice(start, end))
}
loadNext()
})
}
async function parseMediaTags(file) {
return new Promise((resolve) => {
jsmediatags.read(file, {
onSuccess: function(tag) {
resolve({
title: tag.tags.title || null,
artist: tag.tags.artist || null,
album: tag.tags.album || null,
})
},
onError: function() {
resolve({ title: null, artist: null, album: null })
}
// await login()
upload(file, currentIndx)
})
})
}
async function uploadFileDirect(file, index, total) {
createProgressItem(file, index, total)
try {
updateProgress(index, '计算文件MD5...', 5)
const md5 = await calculateMD5(file)
const fileSize = file.size
const filename = file.name
updateProgress(index, '解析音频元数据...', 8)
const mediaTags = await parseMediaTags(file)
updateProgress(index, '获取上传凭证...', 10)
const tokenRes = await axios({
method: 'post',
url: `/cloud/upload/token?time=${Date.now()}`,
data: {
cookie: localStorage.getItem('cookie'),
md5: md5,
fileSize: fileSize,
filename: filename,
},
})
if (tokenRes.data.code !== 200) {
throw new Error(tokenRes.data.msg || '获取上传凭证失败')
}
const tokenData = tokenRes.data.data
if (!tokenData.needUpload) {
updateProgress(index, '文件已存在,直接导入云盘...', 80)
await completeUpload(tokenData, file, mediaTags)
updateProgress(index, '上传完成!', 100)
return
}
updateProgress(index, '开始上传到云存储...', 15)
await axios({
method: 'post',
url: tokenData.uploadUrl,
headers: {
'x-nos-token': tokenData.uploadToken,
'Content-MD5': md5,
'Content-Type': 'audio/mpeg',
'Content-Length': String(fileSize),
},
data: file,
onUploadProgress: (progressEvent) => {
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 70) + 15
updateProgress(index, `上传中... ${Math.round(progressEvent.loaded / progressEvent.total * 100)}%`, Math.min(percent, 85))
},
maxContentLength: Infinity,
maxBodyLength: Infinity,
timeout: 600000,
})
updateProgress(index, '上传完成,正在导入云盘...', 90)
await completeUpload(tokenData, file, mediaTags)
updateProgress(index, '上传完成!', 100)
} catch (err) {
console.error(`${file.name} 上传失败:`, err)
const errorMsg = err.response?.data?.msg || err.message || '未知错误'
updateProgress(index, `上传失败: ${errorMsg}`, 0, true)
}
}
async function completeUpload(tokenData, file, mediaTags = {}) {
const songName = mediaTags.title || file.name.replace(/\.[^.]+$/, '')
const artist = mediaTags.artist || '未知艺术家'
const album = mediaTags.album || '未知专辑'
const completeRes = await axios({
method: 'post',
url: `/cloud/upload/complete?time=${Date.now()}`,
data: {
cookie: localStorage.getItem('cookie'),
songId: tokenData.songId,
resourceId: tokenData.resourceId,
md5: tokenData.md5,
filename: file.name,
song: songName,
artist: artist,
album: album,
},
})
if (completeRes.data.code !== 200) {
throw new Error(completeRes.data.msg || '导入云盘失败')
}
return completeRes.data
}
</script>
<script src="https://fastly.jsdelivr.net/npm/[email protected]/spark-md5.min.js"></script>
</body>
</html>
+2 -2
View File
@@ -1,6 +1,6 @@
# 网易云音乐 API Enhanced Reborn
# 网易云音乐 API Enhanced
> 为停更的网易云音乐 NodeJs API 提供持续的维护!
> 🔍 网易云音乐API Node.js服务的复兴项目
- 基于原版网易云API新增更多有趣的功能
- 具备登录接口,多达200多个接口
+496 -34
View File
@@ -1,6 +1,8 @@
# NeteaseCloudMusicApiEnhanced
网易云音乐 NodeJS API Enhanced(Reborn)
网易云音乐 NodeJS API Enhanced
最后更新于: 2026.2.15
## 灵感来自
@@ -59,13 +61,13 @@ $ set HOST=127.0.0.1 && node app.js
支持 npx 方式运行,会自动安装依赖和运行
```
npx @neteaseapireborn/api@版本号
npx @neteasecloudmusicapienhanced/api@版本号
```
或者运行
```
npx @neteaseapireborn/api@latest
npx @neteasecloudmusicapienhanced/api@latest
```
@@ -75,6 +77,8 @@ npx @neteaseapireborn/api@latest
v4.0.8 加入了 Vercel 配置文件,可以直接在 Vercel 下部署了,不需要自己的服务器(访问 Vercel 部署的接口,需要额外加一个 realIP 参数,如 `/song/url?id=1969519579&realIP=116.25.146.177`)
v4.29.9 加入了生成随机中国 IP 功能, 在请求时加上 `randomCNIP=true` 即可使用随机中国 IP, 如 `/song/url?id=1969519579&randomCNIP=true`
不能正常访问的,绑定下国内备案过的域名,之后即可正常访问
### 操作方法
@@ -133,7 +137,10 @@ v3.3.0 后支持使用 PAC 代理,如 `?proxy=http://192.168.0.1/proxy.pac`
v3.31.0 后支持 Node.js 调用,导入的方法为`module`内的文件名,返回内容包含`status`和`body`,`status`为状态码,`body`为请求返回内容,参考`module_example` 文件夹下的 `test.js`
```js
const { login_cellphone, user_cloud } = require('@neteaseapireborn/api')
const {
login_cellphone,
user_cloud,
} = require('@neteasecloudmusicapienhanced/api')
async function main() {
try {
const result = await login_cellphone({
@@ -156,15 +163,13 @@ main()
```ts
// test.ts
import { banner } from '@neteaseapireborn/api'
import { banner } from '@neteasecloudmusicapienhanced/api'
banner({ type: 0 }).then((res) => {
console.log(res)
})
```
## Docker 容器运行 (原版 API, 不推荐)
> 注意: 该部分为**原版网易云音乐 API**的 Docker 容器部署方式, 关于增强版本请**自行构建并部署**
## Docker 容器运行
> 注意: 在 docker 中运行的时候, 由于使用了 request 来发请求, 所以会检查几个 proxy 相关的环境变量(如下所列), 这些环境变量 会影响到 request 的代理, 详情请参考[request 的文档](https://github.com/request/request#proxies), 如果这些环境变量 指向的代理不可用, 那么就会造成错误, 所以在使用 docker 的时候一定要注意这些环境变量. 不过, 要是你在 query 中加上了 proxy 参数, 那么环境变量会被覆盖, 就会用你通过 proxy 参数提供的代理了.
@@ -178,20 +183,20 @@ request 相关的环境变量
6. NO_PROXY
```shell
docker pull binaryify/netease_cloud_music_api
docker pull moefurina/ncm-api
docker run -d -p 3000:3000 --name netease_cloud_music_api binaryify/netease_cloud_music_api
docker run -d -p 3000:3000 --name ncm-api-enhanced moefurina/ncm-api
// 或者
docker run -d -p 3000:3000 binaryify/netease_cloud_music_api
docker run -d -p 3000:3000 moefurina/ncm-api
// 去掉或者设置相关的环境变量
docker run -d -p 3000:3000 --name netease_cloud_music_api -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= binaryify/netease_cloud_music_api
docker run -d -p 3000:3000 --name ncm-api-enhanced -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= moefurina/ncm-api
// 或者
docker run -d -p 3000:3000 -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= binaryify/netease_cloud_music_api
docker run -d -p 3000:3000 -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= moefurina/ncm-api
```
### 自行构建 docker 镜像 (推荐)
@@ -204,6 +209,12 @@ $ sudo docker build . -t netease-music-api
$ sudo docker run -d -p 3000:3000 netease-music-api
```
## 调试工具
- `eapi` 请求参数或返回内容可在 `/eapi_decrypt.html` 里解析
- 请求参数模式下, 解密结果可直接带到 `/api.html` 继续调试
- 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持
## 接口文档
### 调用前须知
@@ -232,6 +243,8 @@ $ sudo docker run -d -p 3000:3000 netease-music-api
!> 由于网易限制,此项目在国外服务器或部分国内云服务上使用会受到限制,如 `460 cheating异常`,如需解决 , 可使用`realIP`参数,传进国内 IP 解决,如:`?realIP=116.25.146.177`
即可解决
!> 接上, v4.29.9 加入了生成随机中国 IP 功能, 在请求时加上 `randomCNIP=true` 即可使用随机中国 IP, 如 `/song/url?id=1969519579&randomCNIP=true`
!> 图片加上 `?param=宽y高` 可控制图片尺寸,如 `http://p4.music.126.net/JzNK4a5PjjPIXAgVlqEc5Q==/109951164154280311.jpg?param=200y200`, `http://p4.music.126.net/JzNK4a5PjjPIXAgVlqEc5Q==/109951164154280311.jpg?param=50y50`
!> 分页接口返回字段里有`more`,more 为 true 则为有下一页
@@ -248,9 +261,11 @@ $ sudo docker run -d -p 3000:3000 netease-music-api
!> ~~因网易增加了网易云盾验证,密码登录暂时不要使用,尽量使用短信验证码登录和二维码登录,否则调用某些接口会触发需要验证的错误~~
!> ~~二开作者注: 现在网易云云盾验证再次升级, 导致现在短信验证码也没法用了~~
!> ~~二开作者再注: 现在二维码登录也无法使用了, 网易云官方最近查的太严了, 现在尝试调用会提示环境异常, 如果各位有绕过的方法请一定开`Pull Request`~~
!> 二开作者再注: 现在二维码登录也无法使用了, 网易云官方最近查的太严了, 现在尝试调用会提示环境异常, 如果各位有绕过的方法请一定开`Pull Request`
!> ~~二开作者注: 二维码登录现在是修复了, 但是密码登录和短信登录还是不行, 如果各位有绕过的方法请一定开`Pull Request`~~
!> 二开作者注: 在`v4.29.18`版本中修复了密码登录和短信登录的问题, 现在可以正常使用了
#### 1. 手机登录
@@ -774,7 +789,7 @@ tags: 歌单标签
**必选参数 :** `uid` : 用户 id
**可选参数 :**
`limit` : 返回数量 , 默认为 30
`limit` : 返回数量 , 默认为 20
`offset` : 偏移数量,用于分页 ,如 :( 页数 -1)\*30, 其中 30 为 limit 的值 , 默认为 0
@@ -1237,7 +1252,7 @@ tags: 歌单标签
**必选参数 :** `id` : 音乐 id
`level`: 播放音质等级, 分为 `standard` => `标准`,`higher` => `较高`, `exhigh`=>`极高`,
`lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清环绕声`, `sky` => `沉浸环绕声`, `dolby` => `杜比全景声`, `jymaster` => `超清母带`
`unblock`: 是否使用`UnblockNeteaseMusic`, 分为`true`和`false`
`unblock`: 是否使用使用歌曲解锁, 分为`true`和`false`
**接口地址 :** `/song/url/v1`
@@ -1245,7 +1260,20 @@ tags: 歌单标签
说明:`杜比全景声`音质需要设备支持,不同的设备可能会返回不同码率的 url。cookie 需要传入`os=pc`保证返回正常码率的 url。
注意: 如果解灰出现问题, 本项目只是集成[UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server)的接口, 请在对应的仓库开启议题
### 302到音乐 url - 新版
说明 : 只允许传入单个`id`,会使用302重定向请求到目标url
**必选参数 :** `id` : 音乐 id
`level`: 播放音质等级, 分为 `standard` => `标准`,`higher` => `较高`, `exhigh`=>`极高`,
`lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清环绕声`, `sky` => `沉浸环绕声`, `dolby` => `杜比全景声`, `jymaster` => `超清母带`
`unblock`: 是否使用使用歌曲解锁, 分为`true`和`false`
**接口地址 :** `/song/url/v1/302`
**调用例子 :** `/song/url/v1/302?id=1969519579&level=exhigh`
说明:`杜比全景声`音质需要设备支持,不同的设备可能会返回不同码率的 url。cookie 需要传入`os=pc`保证返回正常码率的 url。
### 音乐是否可用
@@ -1261,17 +1289,15 @@ tags: 歌单标签
### 直接获取灰色歌曲链接
说明 : 技术来自于 [UnblockNeteaseMusic](https://github.com/unblockneteasemusic/server) 的支持, 聚合多个音源匹配(有几率匹配不成功)
说明 : 技术部分来自于 [UnblockNeteaseMusic](https://github.com/unblockneteasemusic/server) 的支持
**必选参数 :** `id` : 音乐 id
**可选参数 :** `source`: 选择要解灰的音源, 目前支持`pyncmd`, `kuwo`, `kugou`, `qq`, `migu`, 选择多个音源中间用`,`分隔
**可选参数 :** `source`: 选择要解灰的音源, 不支持多音源
**接口地址 :** `/song/url/match`
**调用例子 :** `/song/url/match?id=1969519579` `/song/url/match?id=1969519579&source=pyncmd,kuwo`
注意: 如果解灰出现问题, 本项目只是集成[UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server)的接口, 请在对应的仓库开启议题
**调用例子 :** `/song/url/match?id=1969519579` `/song/url/match?id=1969519579`
### 搜索
@@ -1364,7 +1390,7 @@ tags: 歌单标签
说明 : 调用此接口 , 传入类型和歌单 id 可收藏歌单或者取消收藏歌单
!> 警告: 在`v4.25.0`版本后, 在网易云登陆后请求不要带上`cookie`字段, 会导致请求不合法
!> 警告: 在`v4.29.7`版本后, 在网易云登陆后请求要带上`timestamp`字段, 否则会导致请求不合法
**必选参数 :**
@@ -1395,7 +1421,7 @@ tags: 歌单标签
说明 : 调用此接口 , 可以添加歌曲到歌单或者从歌单删除某首歌曲 ( 需要登录 )
!> 警告: 在`v4.25.0`版本后, 在网易云登陆后请求不要带上`cookie`字段, 会导致请求不合法
!> 警告: 在`v4.29.7`版本后, 在网易云登陆后请求要带上`timestamp`字段, 否则会导致请求不合法
**必选参数 :**
@@ -1681,6 +1707,62 @@ tags: 歌单标签
**调用例子 :** `/comment/video?id=89ADDE33C0AAE8EC14B99F6750DB954D`
### 评论统计数据
说明 : 调用此接口 , 传入资源类型和资源 id 列表 , 可批量获取对应资源的评论统计数据 ( 不需要登录 )
**必选参数 :**
`type`: 数字 , 资源类型 , 对应以下类型
```
0: 歌曲
1: mv
2: 歌单
3: 专辑
4: 电台节目
5: 视频
6: 动态
7: 电台
```
`ids`: 资源 id 列表 , 多个 id 用逗号分隔 , 如 `186016,347230`
**接口地址 :** `/comment/info/list`
**调用例子 :** `/comment/info/list?type=0&ids=186016,347230`
**返回数据 :**
```json
{
"data": [
{
"latestLikedUsers": null,
"liked": false,
"comments": null,
"resourceType": 4,
"resourceId": 186016,
"commentUpgraded": false,
"musicianSaidCount": 0,
"commentCountDesc": "100w+",
"likedCount": 347,
"commentCount": 1970844,
"shareCount": 109721,
"threadId": "R_SO_4_186016"
}
],
"code": 200
}
```
### 热门评论
说明 : 调用此接口 , 传入 type, 资源 id 可获得对应资源热门评论 ( 不需要登录 )
@@ -2248,6 +2330,8 @@ privilege:权限相关信息
说明 : 调用此接口 , 可获得每日推荐歌曲 ( 需要登录 )
**可选参数 :** `afresh`: 是否刷新日推 , 默认为 false
**接口地址 :** `/recommend/songs`
**调用例子 :** `/recommend/songs`
@@ -2763,7 +2847,7 @@ type : 地区
参考: https://github.com/neteasecloudmusicapienhanced/api-enhanced/blob/main/public/cloud.html
访问地址: http://localhost:3000/cloud.html)
访问地址: http://localhost:3000/cloud.html
支持命令行调用,参考 module_example 目录下`song_upload.js`
@@ -2771,6 +2855,78 @@ type : 地区
**调用例子 :** `/cloud`
#### 上传模式说明
云盘上传支持两种模式:
**1. 后端代理模式 (默认)**
文件通过服务器转发到云存储,调用简单,但受服务器限制:
- Vercel Serverless Functions 限制请求体大小为 4.5MB
- 自建服务器需配置足够大的请求体限制
**2. 客户端直传模式 (推荐用于 Vercel)**
文件直接从客户端上传到云存储服务器,绕过服务器限制:
- 支持大文件上传
- 适合 Vercel、Netlify 等有请求体限制的平台
- 需要前端配合实现
#### 客户端直传相关接口
**获取上传凭证**
**接口地址 :** `/cloud/upload/token`
**必选参数 :**
- `cookie`: 网易云音乐 Cookie (在请求体中传递)
- `md5`: 文件 MD5 值
- `fileSize`: 文件大小(字节)
- `filename`: 文件名
**返回数据 :**
```json
{
"code": 200,
"data": {
"needUpload": true,
"songId": "...",
"uploadToken": "...",
"uploadUrl": "...",
"resourceId": "..."
}
}
```
**完成上传导入**
**接口地址 :** `/cloud/upload/complete`
**必选参数 :**
- `cookie`: 网易云音乐 Cookie (在请求体中传递)
- `songId`: 歌曲 ID
- `resourceId`: 资源 ID
- `md5`: 文件 MD5
- `filename`: 文件名
**可选参数 :**
- `song`: 歌曲名
- `artist`: 艺术家
- `album`: 专辑名
#### 客户端直传流程
1. 客户端计算文件 MD5
2. 调用 `/cloud/upload/token` 获取上传凭证
3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl`
4. 调用 `/cloud/upload/complete` 完成导入
### 云盘歌曲信息匹配纠正
说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0
@@ -2786,6 +2942,20 @@ type : 地区
**调用例子 :** `/cloud/match?uid=32953014&sid=aaa&asid=bbb` `/cloud/match?uid=32953014&sid=bbb&asid=0`
### 获取云盘歌词
说明: 调用此接口, 获取云盘歌曲的歌词,歌词来自此文件的音乐元数据`LYRICS`标签。
**可选参数 :**
`uid`: 用户 id
`sid`: 云盘的歌曲 id
**接口地址:** `/cloud/lyric/get`
**调用例子:** `/cloud/lyric/get?uid=1&sid=aaa`
### 电台 banner
说明 : 调用此接口,可获取电台 banner
@@ -3617,6 +3787,14 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
**调用例子 :** `/musician/tasks/new`
### 音乐人黑胶会员任务
说明 : 音乐人登录后调用此接口 , 可获取音乐人黑胶会员任务。返回的数据中`missionStatus`字段为任务状态,100 表示任务完成。
**接口地址 :** `/musician/vip/tasks`
**调用例子 :** `/musician/vip/tasks`
### 账号云豆数
说明 : 音乐人登录后调用此接口 , 可获取账号云豆数
@@ -3948,13 +4126,15 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
**接口地址:** `/voicelist/search`
**必选参数:**
`keyword`: 搜索关键词
**可选参数:**
`limit`: 取出歌单数量 , 默认为 200
`limit`: 取出歌单数量, 默认为 10
`offset`: 偏移数量 , 用于分页 , 如 :( 评论页数 -1)\*200, 其中 200 为 limit 的值
`podcastName`: 播客名称
`offset`: 偏移数量 , 用于分页 , 默认为 30
### 播客声音列表
@@ -3987,7 +4167,6 @@ ONLINE 已发布
**可选参数**
- 状态(非必填):
- `displayStatus: null`(默认):返回所有状态的声音
- `displayStatus: "ONLINE"`:已发布的声音
- `displayStatus: "AUDITING"`:审核中的声音
@@ -4000,19 +4179,16 @@ ONLINE 已发布
- `limit: 20`:每次返回的声音数量(最多 200 个)
- 搜索关键词:
- `name: null`:返回所有的声音
- `name: [关键词]`:返回包含指定关键词的声音文件
- `offset: 0`:偏移量,用于分页,默认为 0,表示从第一个声音开始获取
- 博客:
- `radioId: null`:返回所有电台的声音
- `radioId: [播客id]`:返回特定播客的声音
- 是否公开:
- `type: null`:返回所有类型的声音
- `type: "PUBLIC"`:返回公开的声音
- `type: "PRIVATE"`:返回隐私的声音
@@ -4847,6 +5023,292 @@ let data = encodeURIComponent(
**调用例子:** `/broadcast/channel/list`
### 黑胶乐签打卡
说明: 登录后调用此接口, 进行黑胶乐签打卡
**接口地址:** `/vip/sign`
**调用例子:** `/vip/sign`
### 黑胶乐签打卡信息
说明: 登录后调用此接口, 获取黑胶乐签打卡信息
**接口地址:** `/vip/sign/info`
**调用例子:** `/vip/sign/info`
### 广播电台 - 收藏/取消收藏电台
说明: 登录后调用此接口, 传入电台 id, 可收藏或取消收藏广播电台
**必选参数:**
`id`: 电台 id
`t`: 操作类型, `1` 为收藏, 其余值为取消收藏
**接口地址:** `/broadcast/sub`
**调用例子:** `/broadcast/sub?id=5&t=1`
### 用户的创建歌单列表
说明 : 调用此接口, 传入用户id, 获取用户的创建歌单列表
**必选参数 :**
`uid`: 用户 id
**可选参数 :**
`limit` : 返回数量 , 默认为 100
`offset` : 偏移数量,用于分页 ,如 :( 页数 -1)\*30, 其中 30 为 limit 的值 , 默认为 0
**接口地址 :** `/user/playlist/create`
**调用例子 :** `/user/playlist/create?uid=32953014`
### 用户的收藏歌单列表
说明 : 调用此接口, 传入用户id, 获取用户的收藏歌单列表
**必选参数 :**
`uid`: 用户 id
**可选参数 :**
`limit` : 返回数量 , 默认为 100
`offset` : 偏移数量,用于分页 ,如 :( 页数 -1)\*30, 其中 30 为 limit 的值 , 默认为 0
**接口地址 :** `/user/playlist/collect`
**调用例子 :** `/user/playlist/collect?uid=32953014`
### 搜索建议 - PC端
说明 : 调用此接口, 传入搜索关键词, 获取搜索建议
**必选参数 :**
`keyword`: 搜索关键词
**接口地址 :** `/search/suggest/pc`
**调用例子 :** `/search/suggest/pc?keyword=海阔天空`
### 喜欢歌曲 - 新版
说明 : 登录后调用此接口, 传入歌曲 id 用户id和喜欢状态, 可喜欢/取消喜欢歌曲
**必选参数 :**
`id`: 歌曲 id
`uid`: 用户 id
`like`: 喜欢状态, true 表示喜欢, false 表示取消喜欢
**接口地址 :** `/song/like`
**调用例子 :** `/song/like?id=2058263032&uid=32953014&like=true`
### 我创建的播客声音
说明 : 登录后调用此接口, 获取我创建的博客声音
**可选参数 :**
`limit` : 返回数量 , 默认为 20
**接口地址 :** `/voicelist/my/created`
**调用例子 :** `/voicelist/my/created`
### DIFM电台 - 分类
说明: 调用此接口, 获取DIFM电台分类
**必选参数 :**
`sources`: 来源列表, 0: 最嗨电音 1: 古典电台 2: 爵士电台
**接口地址:** `/dj/difm/all/style/channel`
**调用例子:** `/dj/difm/all/style/channel?sources=[0]`
### DIFM电台 - 收藏列表
说明: 调用此接口, 获取DIFM电台收藏列表
**必选参数 :**
`sources`: 来源列表, 0: 最嗨电音 1: 古典电台 2: 爵士电台
**接口地址:** `/dj/difm/subscribe/channels/get`
**调用例子:** `/dj/difm/subscribe/channels/get?sources=[0]`
### DIFM电台 - 收藏频道
说明: 调用此接口, 可收藏DIFM频道
**必选参数 :**
`id`: 频道id
**接口地址:** `/dj/difm/channel/subscribe`
**调用例子:** `/dj/difm/channel/subscribe?id=1`
### DIFM电台 - 取消收藏频道
说明: 调用此接口, 可取消收藏DIFM频道
**必选参数 :**
`id`: 频道id
**接口地址:** `/dj/difm/channel/unsubscribe`
**调用例子:** `/dj/difm/channel/unsubscribe?id=1`
### DIFM电台 - 播放列表
说明: 调用此接口, 获取DIFM播放列表
**必选参数 :**
`source`: 来源, 0: 最嗨电音 1: 古典电台 2: 爵士电台
`channelId`: 频道id
**可选参数 :**
`limit`: 返回数量, 默认为 5
**接口地址:** `/dj/difm/playing/tracks/list`
**调用例子:** `/dj/difm/playing/tracks/list?source=0&channelId=1012`
### 助眠解压 - 特定时间场景下的推荐资源
说明: 调用此接口, 获取特定时间场景下的推荐资源
**接口地址:** `/sati/timescene/resources/get`
**调用例子:** `/sati/timescene/resources/get`
### 助眠解压 - 标签列表
说明: 调用此接口, 获取标签列表
**接口地址:** `/sati/tag/list`
**调用例子:** `/sati/tag/list`
### 助眠解压 - 获取标签下资源列表
说明: 调用此接口, 获取标签下资源列表; 接口返回的`trackId`可以用于请求`/song/url/v1`接口,用于获取声音的下载地址
**必选参数 :**
`tag`: 标签, 由标签列表接口得到
**接口地址:** `/sati/resource/list`
**调用例子:** `/sati/resource/list?tag=naturalMusic`
### 助眠解压 - 查看同类推荐
说明: 调用此接口, 查看同类推荐
**必选参数 :**
`id`: id, `/sati/tag/list`接口返回的`trackId`
**接口地址:** `/sati/resource/list/more`
**调用例子:** `/sati/resource/list/more?id=167003`
### 助眠解压 - 收藏列表
说明: 调用此接口, 获取收藏列表
**接口地址:** `/sati/resource/sub/list`
**调用例子:** `/sati/resource/sub/list`
### 助眠解压 - 收藏
说明: 调用此接口, 收藏声音
**必选参数 :**
`id`: id, `/sati/tag/list`接口返回的`trackId`
**可选参数 :**
`cancel`: 是否取消收藏, 默认不取消
**接口地址:** `/sati/resource/sub`
**调用例子:** `/sati/resource/sub?id=167003`
### 跑步漫游
说明: 调用此接口,获取跑步漫游的歌曲信息
**必选参数:**
`bpm`: 步频
**接口地址:** `/radio/sport/get`
**调用例子:** `/radio/sport/get?bpm=50`
### 歌曲创作者信息
说明 : 调用此接口, 传入音乐 id 可获得对应音乐的创作者信息
**必选参数 :** `id`: 音乐 id
**接口地址 :** `/song/creators`
**调用例子 :** `/song/creators?id=33894312`
### 灰色歌曲的其他版本推荐
说明 : 调用此接口, 传入灰色歌曲 id, 获取该歌曲的其他可播放版本推荐
**必选参数 :**
`songid`: 歌曲 id, 可使用 `id` 代替
**接口地址 :** `/song/copyright/rcmd`
**调用例子 :** `/song/copyright/rcmd?songid=27946878`
### 举报评论
说明 : 登录后调用此接口, 传入歌曲 id 和评论 id, 举报评论
**必选参数 :**
`id`: 歌曲 id
`cid`: 评论 id
`reason`: 举报理由
**接口地址 :** `/comment/report`
**调用例子 :* `/comment/report?id=2058263032&cid=123456789&reason=人身攻击`
## 离线访问此文档
此文档同时也是 Progressive Web Apps(PWA), 加入了 serviceWorker, 可离线访问
+88 -6
View File
@@ -5,11 +5,54 @@
<meta name="KEYWords" contect="网易云音乐,网易云音乐 api,网易云音乐 nodejs,网易云音乐 node.js">
<meta name="description" content="网易云音乐 NodeJS API Enhanced">
<title>网易云音乐 NodeJS API Enhanced</title>
<link rel="icon" href="favicon.ico">
<link rel="icon" href="netease.png">
<meta name="description" content="Description">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta name="referrer" content="never">
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css" id="theme-css">
<style>
.theme-toggle {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 999;
width: 44px;
height: 44px;
border-radius: 50%;
border: 1px solid rgba(255,255,255,0.1);
background: rgba(30,30,46,0.75);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
color: #fff;
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
transition: all 0.3s cubic-bezier(0.4,0,0.2,1);
outline: none;
padding: 0;
line-height: 1;
user-select: none;
}
.theme-toggle:hover {
transform: scale(1.12) rotate(12deg);
box-shadow: 0 4px 20px rgba(0,0,0,0.25);
}
.theme-toggle:active {
transform: scale(0.92);
}
.theme-toggle.light {
background: rgba(255,255,255,0.85);
color: #333;
border-color: rgba(0,0,0,0.08);
}
html, body, .sidebar, .content, .markdown-section,
.sidebar-nav, .search, .docsify-pagination-container {
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
</style>
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
(adsbygoogle = window.adsbygoogle || []).push({
@@ -20,13 +63,52 @@
</head>
<body>
<div id="app"></div>
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme">🌙</button>
</body>
<script>
(function() {
var STORAGE_KEY = 'docsify-theme';
var link = document.getElementById('theme-css');
var btn = document.getElementById('themeToggle');
var LIGHT_URL = '//unpkg.com/docsify/lib/themes/vue.css';
var DARK_URL = '//unpkg.com/docsify/lib/themes/dark.css';
function setTheme(theme) {
link.href = theme === 'dark' ? DARK_URL : LIGHT_URL;
btn.textContent = theme === 'dark' ? '☀️' : '🌙';
btn.className = 'theme-toggle' + (theme === 'dark' ? '' : ' light');
localStorage.setItem(STORAGE_KEY, theme);
}
function toggleTheme() {
var isDark = link.href.includes('dark');
setTheme(isDark ? 'light' : 'dark');
}
var saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
setTheme(saved);
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
setTheme('dark');
}
btn.addEventListener('click', toggleTheme);
try {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
if (!localStorage.getItem(STORAGE_KEY)) {
setTheme(e.matches ? 'dark' : 'light');
}
});
} catch(e) {}
})();
</script>
<script>
window.$docsify = {
name: '网易云音乐 API Enhanced',
repo: 'https://github.com/neteasecloudmusicapienhanced/api-enhanced',
coverpage: true,
homepage: 'home.md',
homepage: 'https://cdn.jsdelivr.net/gh/NeteaseCloudMusicApiEnhanced/api-enhanced@main/public/docs/home.md',
}
</script>
<script src="https://unpkg.com/[email protected]/lib/docsify.min.js"></script>
@@ -36,12 +118,12 @@
}
</script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-139996012-1"></script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-BPRGR23JEG"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'UA-139996012-1');
gtag('config', 'G-BPRGR23JEG');
</script>
</html>
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M10.421 11.375c-.294 1.028.012 2.064.784 2.653 1.061.81 2.565.3 2.874-.995.08-.337.103-.722.027-1.056-.23-1.001-.52-1.988-.792-2.996-1.33.154-2.543 1.172-2.893 2.394zm5.548-.287c.273 1.012.285 2.017-.127 3-1.128 2.69-4.721 3.14-6.573.826-1.302-1.627-1.28-3.961.06-5.734.78-1.032 1.804-1.707 3.048-2.054l.379-.104c-.084-.415-.188-.816-.243-1.224-.176-1.317.512-2.503 1.744-3.04 1.226-.535 2.708-.216 3.53.76.406.479.395 1.08-.025 1.464-.412.377-.996.346-1.435-.09-.247-.246-.51-.44-.877-.436-.525.006-.987.418-.945.937.037.468.173.93.3 1.386.022.078.216.135.338.153 1.334.197 2.504.731 3.472 1.676 2.558 2.493 2.861 6.531.672 9.44-1.529 2.032-3.61 3.168-6.127 3.409-4.621.44-8.664-2.53-9.7-7.058C2.515 10.255 4.84 5.831 8.795 4.25c.586-.234 1.143-.031 1.371.498.232.537-.019 1.086-.61 1.35-2.368 1.06-3.817 2.855-4.215 5.424-.533 3.433 1.656 6.776 5 7.72 2.723.77 5.658-.166 7.308-2.33 1.586-2.08 1.4-5.099-.427-6.873a3.979 3.979 0 0 0-1.823-1.013c.198.716.389 1.388.57 2.062z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+215 -27
View File
@@ -1,37 +1,186 @@
<!DOCTYPE html>
<html lang="en">
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>eapi 参数和返回内容解析</title>
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
<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: 900px;
margin: 0 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: 24px;
}
.form-group {
margin-bottom: 24px;
}
label {
display: block;
font-size: 14px;
font-weight: 500;
color: #555;
margin-bottom: 8px;
}
textarea {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 13px;
resize: vertical;
min-height: 200px;
outline: none;
}
textarea:focus {
border-color: #333;
}
.radio-group {
display: flex;
gap: 24px;
margin-bottom: 24px;
}
.radio-item {
display: flex;
align-items: center;
gap: 8px;
}
.radio-item input[type="radio"] {
cursor: pointer;
}
.radio-item label {
margin: 0;
cursor: pointer;
font-size: 14px;
}
button {
background: #333;
color: white;
padding: 12px 28px;
border: none;
border-radius: 6px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s ease;
}
button + button {
margin-left: 12px;
}
button:hover {
background: #555;
}
.result-section {
margin-top: 24px;
}
.result-section label {
margin-bottom: 12px;
}
.decode-result {
white-space: pre-wrap;
word-break: break-all;
background: #f9f9f9;
padding: 16px;
border-radius: 6px;
border: 1px solid #eee;
min-height: 200px;
max-height: 400px;
overflow: auto;
font-family: 'Courier New', monospace;
font-size: 13px;
}
.example-section {
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid #eee;
}
.example-section h2 {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
}
.example-section img {
max-width: 100%;
height: auto;
border-radius: 6px;
margin-bottom: 16px;
border: 1px solid #eee;
}
</style>
</head>
<body>
<div id="app" class="p-5 flex flex-col">
<h1 class="text-2xl font-bold mb-5">eapi 参数和返回内容解析</h1>
<textarea class="border border-gray-300 p-3 mb-5" v-model="hexString" rows="10"></textarea>
<button @click="decrypt" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
解密
</button>
<div class="mt-3">
<input type="radio" id="format" name="format" v-model="isReq" value="true">
<label for="format" class="ml-2">请求数据request params(针对请求数据的 params)</label>
<input type="radio" id="noFormat" name="format" v-model="isReq" value="false" class="ml-5">
<label for="noFormat" class="ml-2">返回数据 response 二进制数据(针对返回内容解析)</label>
</div>
<div>
<p>解密结果:
<pre>{{ JSON.stringify(JSON.parse(result), null, 2) }}</pre>
</p>
<div id="app" class="container">
<h1>eapi 参数和返回内容解析</h1>
<div class="form-group">
<label for="hexString">十六进制字符串</label>
<textarea id="hexString" v-model="hexString" rows="10"></textarea>
</div>
<div class="radio-group">
<div class="radio-item">
<input type="radio" id="req" name="format" v-model="isReq" value="true">
<label for="req">请求数据 request params</label>
</div>
<div class="radio-item">
<input type="radio" id="resp" name="format" v-model="isReq" value="false">
<label for="resp">返回数据 response 二进制数据</label>
</div>
</div>
<div>
<p>使用例子:</p>
<img src="/static/eapi_params.png" />
<img src="/static/eapi_response.png" />
<button @click="decrypt">解密</button>
<button @click="sendToApi" :disabled="!canSend" :class="[{ 'opacity-50 cursor-not-allowed pointer-events-none': !canSend },]">填入 API 调试</button>
<div class="result-section">
<label>解密结果:</label>
<pre class="decode-result">{{ formatResult(result) }}</pre>
</div>
<div class="example-section">
<h2>使用示例</h2>
<img src="/static/eapi_params.png" alt="请求示例" />
<img src="/static/eapi_response.png" alt="响应示例" />
</div>
</div>
@@ -39,7 +188,6 @@
<script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
<script>
const logger = require('../util/logger.js')
const app = Vue.createApp({
data() {
return {
@@ -51,7 +199,31 @@
mounted() {
this.decrypt()
},
computed: {
isRequestMode() {
return this.isReq === true || this.isReq === 'true'
},
canSend() {
if (!this.isRequestMode) return false
if (!this.result || this.result === '{}' || this.result === 'null') return false
try {
JSON.parse(this.result)
return true
} catch (error) {
return false
}
},
},
methods: {
formatResult(value) {
if (value == null || value === '') return ''
try {
const parsed = typeof value === 'string' ? JSON.parse(value) : value
return JSON.stringify(parsed, null, 2)
} catch (error) {
return String(value)
}
},
async decrypt() {
try {
const res = await axios({
@@ -62,16 +234,32 @@
}
})
this.result = JSON.stringify(res.data.data)
logger.info(res.data);
console.log(res.data);
} catch (error) {
console.error(error)
alert(error?.response?.data?.message || '解密失败,数据格式错误')
}
}
},
sendToApi() {
if (!this.canSend) return
const payload = JSON.parse(this.result)
const params = new URLSearchParams()
params.set('uri', payload.uri || payload.url || payload.path || '')
params.set('crypto', 'eapi')
const data =
payload.params ||
payload.data ||
payload.body ||
payload.payload ||
payload.request ||
{}
params.set('data', JSON.stringify(data))
window.open(`/api.html?${params.toString()}`, '_blank')
},
}
})
app.mount('#app')
</script>
</body>
</html>
</html>
+1 -2
View File
@@ -12,7 +12,6 @@
</div>
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
<script>
const logger = require('../util/logger.js')
async function main() {
const res = await axios({
url: `/homepage/block/page`,
@@ -22,7 +21,7 @@
method: 'post',
})
let cursor = ''
logger.info(res.data.data)
console.info(res.data.data)
if (res.data.data.hasMore) {
cursor = res.data.data.cursor
const res2 = await axios({
+99 -287
View File
@@ -1,312 +1,124 @@
<!DOCTYPE html>
<html lang="zh">
<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">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="icon" href="docs/netease.png">
<title>网易云音乐 API Enhanced</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/[email protected]/css/materialdesignicons.min.css">
<style>
:root {
--primary-color: #2d8cf0;
--secondary-color: #42b983;
--text-color: #333;
--text-secondary: #666;
--bg-color: #f5f7fa;
--card-bg: rgba(255,255,255,0.95);
--hover-bg: #eaf4fb;
--border-radius: 12px;
--transition: all 0.3s ease;
--shadow: 0 8px 24px rgba(31, 38, 135, 0.12);
--container-width: 1200px;
--fg: #333;
--muted: #666;
--border: #ddd;
--bg: #f5f5f5;
--panel: #ffffff;
--accent: #333;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
* { 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: 960px; 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; }
}
html, body {
height: 100%;
font-family: 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', Arial, sans-serif;
background: linear-gradient(135deg, #e0eafc 0%, #cfdef3 100%);
color: var(--text-color);
line-height: 1.6;
header.site-header { margin-bottom: 24px; }
header.site-header h1 { font-size: 28px; font-weight: 600; margin: 0; }
.badge { display: inline-block; margin-left: 8px; padding: 4px 10px; border: 1px solid var(--border); border-radius: 12px; font-size: 12px; color: var(--muted); }
.sub { margin-top: 8px; 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 h2 { margin: 0 0 12px; font-size: 18px; font-weight: 600; }
.kvs { display: grid; grid-template-columns: 100px 1fr; gap: 8px 12px; align-items: start; }
.kvs div:first-child { color: var(--muted); flex-shrink: 0; }
.kvs div:last-child { word-break: break-all; overflow-wrap: anywhere; min-width: 0; overflow: hidden; }
@media (max-width: 480px) {
.kvs { grid-template-columns: 1fr; gap: 4px 12px; }
.kvs div:first-child { font-weight: 500; }
}
.layout {
min-height: 100%;
padding: 2rem 1rem;
display: flex;
flex-direction: column;
align-items: center;
}
.container {
width: 100%;
max-width: var(--container-width);
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
animation: fadeIn 0.8s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.header {
grid-column: 1 / -1;
text-align: center;
padding: 2rem;
background: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
}
.header h1 {
font-size: 2.5rem;
color: var(--primary-color);
margin-bottom: 1rem;
text-shadow: 0 2px 8px #e0eafc;
}
.header p {
color: var(--text-secondary);
font-size: 1.1rem;
max-width: 800px;
margin: 0 auto;
}
.card {
background: var(--card-bg);
border-radius: var(--border-radius);
padding: 1.5rem;
box-shadow: var(--shadow);
transition: var(--transition);
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 32px rgba(31, 38, 135, 0.15);
}
.card h2 {
color: var(--primary-color);
font-size: 1.5rem;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.card h2 i {
font-size: 1.8rem;
}
.feature-list {
list-style: none;
}
.feature-item {
display: flex;
align-items: center;
padding: 0.8rem;
margin: 0.5rem 0;
background: var(--bg-color);
border-radius: 8px;
transition: var(--transition);
}
.feature-item:hover {
background: var(--hover-bg);
transform: translateX(5px);
}
.feature-item a {
color: var(--text-color);
text-decoration: none;
flex: 1;
display: flex;
align-items: center;
gap: 0.5rem;
}
.feature-item i {
color: var(--primary-color);
font-size: 1.2rem;
}
.status {
background: var(--card-bg);
padding: 1rem;
border-radius: var(--border-radius);
margin-top: 2rem;
text-align: center;
font-size: 0.9rem;
color: var(--text-secondary);
}
.version {
display: inline-block;
padding: 0.2rem 0.8rem;
background: var(--primary-color);
color: white;
border-radius: 20px;
font-size: 0.9rem;
margin-left: 1rem;
}
.footer {
grid-column: 1 / -1;
text-align: center;
margin-top: 2rem;
padding: 1rem;
color: var(--text-secondary);
}
.footer a {
color: var(--primary-color);
text-decoration: none;
transition: var(--transition);
}
.footer a:hover {
color: var(--secondary-color);
}
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr;
}
.header h1 {
font-size: 2rem;
}
.card {
margin: 0 1rem;
}
ul.links { list-style: none; padding: 0; margin: 0; }
ul.links li { margin: 8px 0; }
ul.links a { color: var(--fg); text-decoration: none; border-bottom: 1px dotted var(--border); transition: all 0.2s ease; }
ul.links a:hover { color: var(--accent); border-bottom-color: var(--accent); }
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; }
code { font-family: 'Courier New', monospace; font-size: 13px; }
@media (max-width: 480px) {
code { font-size: 12px; }
}
footer.site-footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); text-align: center; }
footer.site-footer a { color: var(--fg); text-decoration: none; transition: color 0.2s ease; }
footer.site-footer a:hover { color: var(--accent); }
</style>
</head>
<body>
<div class="layout">
<div class="container">
<header class="header">
<h1>网易云音乐 API Enhanced <span id="api-version" class="version"></span></h1>
<p>🔍 A revival project for NeteaseCloudMusicApi Node.js Services</p>
</header>
<main class="container">
<header class="site-header">
<h1>网易云音乐 API Enhanced <span id="api-version" class="badge"></span></h1>
<p class="sub">🔍 A revival project for NeteaseCloudMusicApi Node.js Api Services || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护</p>
</header>
<div class="card">
<h2><i class="mdi mdi-book-open-page-variant"></i>文档与演示</h2>
<ul class="feature-list">
<li class="feature-item">
<a href="/docs" target="_blank">
<i class="mdi mdi-file-document"></i>
API 文档
</a>
</li>
<li class="feature-item">
<a href="./api.html">
<i class="mdi mdi-console"></i>
API 调试界面
</a>
</li>
<li class="feature-item">
<a href="./qrlogin.html">
<i class="mdi mdi-qrcode"></i>
二维码登录演示
</a>
</li>
</ul>
<section class="block">
<h2>状态</h2>
<div class="kvs">
<div>Base URL</div><div id="base-url">—</div>
<div>当前页</div><div id="current-url">—</div>
</div>
</section>
<div class="card">
<h2><i class="mdi mdi-music"></i>音乐功能</h2>
<ul class="feature-list">
<li class="feature-item">
<a href="./search?keywords=这么可爱真是抱歉">
<i class="mdi mdi-magnify"></i>
搜索歌曲
</a>
</li>
<li class="feature-item">
<a href="./song/url/unblock?id=1969519579">
<i class="mdi mdi-lock-open"></i>
歌曲解灰
</a>
</li>
<li class="feature-item">
<a href="./comment/music?id=1969519579&limit=1">
<i class="mdi mdi-comment"></i>
获取评论
</a>
</li>
</ul>
<section class="block">
<h2>文档</h2>
<p><a href="/docs" target="_blank">查看在线文档</a></p>
</section>
<section class="block">
<h2>常用接口</h2>
<ul class="links">
<li><a href="/search?keywords=妖精小姐的魔法邀约">搜索音乐: <code>GET /search</code></a></li>
<li><a href="/song/detail?ids=2756058128">获取音乐详情: <code>GET /song/detail</code></a></li>
<li><a href="/comment/music?id=2756058128&limit=1">获取音乐评论: <code>GET /comment/music</code></a></li>
<li><a href="/song/url/v1?id=2756058128&level=exhigh">获取音乐播放链接: <code>GET /song/url/v1</code></a></li>
</ul>
</section>
<section class="block">
<h2>调试部分</h2>
<pre><code>curl -s {origin}/inner/version
curl -s {origin}/search?keywords=网易云</code></pre>
<div style="margin-top:10px; line-height:2;">
<a href="/api.html">交互式调试</a> ·
<a href="/qrlogin.html">二维码登录示例</a> ·
<a href="/unblock_test.html">解灰测试</a> ·
<a href="/audio_match_demo/index.html">听歌识曲 Demo</a> ·
<a href="/cloud.html">云盘上传</a> ·
<a href="/playlist_import.html">歌单导入</a> ·
<a href="/eapi_decrypt.html">EAPI 解密</a> ·
<a href="/listen_together_host.html">一起听示例</a> ·
<a href="/playlist_cover_update.html">更新歌单封面示例</a> ·
<a href="/avatar_update.html">头像更新示例</a>
</div>
</section>
<div class="card">
<h2><i class="mdi mdi-tools"></i>实用工具</h2>
<ul class="feature-list">
<li class="feature-item">
<a href="./audio_match_demo/index.html">
<i class="mdi mdi-music-note-search"></i>
听歌识曲
</a>
</li>
<li class="feature-item">
<a href="./cloud.html">
<i class="mdi mdi-cloud-upload"></i>
云盘上传
</a>
</li>
<li class="feature-item">
<a href="./playlist_import.html">
<i class="mdi mdi-playlist-plus"></i>
歌单导入
</a>
</li>
<li class="feature-item">
<a href="./eapi_decrypt.html">
<i class="mdi mdi-decode"></i>
EAPI 解析
</a>
</li>
</ul>
</div>
<footer class="site-footer">
<a href="https://github.com/neteasecloudmusicapienhanced/api-enhanced" target="_blank">GitHub</a>
</footer>
</main>
<div class="status">
当前访问地址:<span id="current-url"></span>
</div>
<footer class="footer">
<span>© 2025 网易云音乐 API Enhanced(Reborn) | </span>
<a href="https://github.com/neteasecloudmusicapienhanced/api-enhanced" target="_blank">
<i class="mdi mdi-github"></i> GitHub
</a>
</footer>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 显示当前URL
document.addEventListener('DOMContentLoaded', function () {
var origin = window.location.origin;
document.getElementById('base-url').textContent = origin;
document.getElementById('current-url').textContent = window.location.href;
// 获取API版本号
axios({
url: '/inner/version',
method: 'post',
data: {},
}).then((res) => {
const version = res.data.data.version;
document.getElementById('api-version').textContent = `v${version}`;
});
fetch('/inner/version', { method: 'POST' })
.then(function (r) { return r.json(); })
.then(function (data) {
var v = data && data.data && data.data.version;
if (v) document.getElementById('api-version').textContent = 'v' + v;
var pre = document.querySelector('pre code');
if (pre) pre.textContent = pre.textContent.replace(/\{origin\}/g, origin);
})
.catch(function () {});
});
</script>
</body>
+305 -77
View File
@@ -1,94 +1,322 @@
<!-- eslint-disable prettier/prettier -->
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>一起听</title>
<title>一起听 - 主机模式</title>
<script src="https://unpkg.com/petite-vue"></script>
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
<link
rel="stylesheet"
href="https://unpkg.com/[email protected]/dist/css/mdui.min.css"
/>
<script src="https://unpkg.com/[email protected]/dist/js/mdui.min.js"></script>
<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: 900px;
margin: 0 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: 16px;
}
.login-link {
display: block;
margin-bottom: 24px;
color: #666;
font-size: 14px;
text-decoration: none;
}
.login-link:hover {
color: #333;
text-decoration: underline;
}
.message {
padding: 12px 16px;
background: #f9f9f9;
border-radius: 6px;
margin-bottom: 20px;
font-size: 14px;
color: #555;
}
.audio-player {
width: 100%;
margin-bottom: 20px;
}
.btn {
background: #333;
color: white;
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s ease;
margin-right: 8px;
margin-bottom: 8px;
}
.btn:hover {
background: #555;
}
.section {
margin-bottom: 24px;
padding: 20px;
background: #f9f9f9;
border-radius: 6px;
}
.section h3 {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
input[type="text"], input[type="number"] {
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
outline: none;
width: 200px;
}
input:focus {
border-color: #333;
}
.input-group {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.input-group label {
font-size: 14px;
color: #555;
min-width: 80px;
}
.share-link {
padding: 12px;
background: #f0f0f0;
border-radius: 6px;
font-size: 13px;
color: #666;
word-break: break-all;
margin-bottom: 12px;
}
.user-list {
list-style: none;
max-height: 200px;
overflow-y: auto;
}
.user-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
border-bottom: 1px solid #eee;
}
.user-item:last-child {
border-bottom: none;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.user-name {
font-size: 14px;
color: #333;
}
.track-list {
list-style: none;
max-height: 300px;
overflow-y: auto;
}
.track-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background 0.2s ease;
}
.track-item:hover {
background: #f5f5f5;
}
.track-item:last-child {
border-bottom: none;
}
.track-cover {
width: 40px;
height: 40px;
border-radius: 4px;
object-fit: cover;
}
.track-name {
font-size: 14px;
color: #333;
}
details {
cursor: pointer;
}
details summary {
font-size: 14px;
font-weight: 500;
color: #555;
padding: 8px 0;
}
details summary:hover {
color: #333;
}
details[open] summary {
margin-bottom: 12px;
}
.control-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
</style>
</head>
<body class="mdui-container">
<div>
<a href="/qrlogin.html"> 如果没登录,请先登录 </a>
</div>
<body class="container">
<a href="/qrlogin.html" class="login-link">还没登录?点击登录</a>
<h1>一起听 - 主机模式</h1>
<div>消息: {{message}}</div>
<audio id="player" autoplay controls></audio>
<br />
<br />
<button v-if="!account.login" @click="login">获取登录状态</button>
<div>您的当前登录账号为: {{account.nickname}}</div>
<br />
<div v-if="account.login">
<button v-if="!roomInfo.roomId" @click="createRoom">创建房间</button>
<div class="message">消息: {{message}}</div>
<audio id="player" class="audio-player" autoplay controls></audio>
<div v-if="!account.login">
<button class="btn" @click="login">获取登录状态</button>
</div>
<div class="section">
<h3>账号信息</h3>
<div>当前登录账号: {{account.nickname}}</div>
</div>
<div v-if="account.login" class="section">
<h3>房间管理</h3>
<div class="control-buttons">
<button v-if="!roomInfo.roomId" class="btn" @click="createRoom">创建房间</button>
</div>
<details>
<summary>加入房间</summary>
<div><span>房间ID: </span><input v-model="roomInfo.roomId" /></div>
<div>
<span>邀请者 ID: </span><input v-model="roomInfo.inviterId" />
<div class="input-group">
<label>房间ID:</label>
<input v-model="roomInfo.roomId" type="text" />
</div>
<button @click="joinRoom">点击加入</button>
<div class="input-group">
<label>邀请者ID:</label>
<input v-model="roomInfo.inviterId" type="text" />
</div>
<button class="btn" @click="joinRoom">加入房间</button>
</details>
<div v-if="roomInfo.roomId">
<div>
分享链接为:
<div v-if="roomInfo.roomId" style="margin-top: 16px;">
<h4>分享链接</h4>
<div class="share-link">
https://st.music.163.com/listen-together/share/?songId=1372188635&roomId={{roomInfo.roomId}}&inviterId={{roomInfo.inviterId}}
</div>
<br />
<button @click="refreshRoom">刷新房间状态</button>
<div>在线用户:</div>
<ul class="mdui-list">
<li
v-for="user in roomInfo.roomUsers"
class="mdui-list-item mdui-ripple"
>
<div class="mdui-list-item-avatar">
<img :src="user.avatarUrl" />
</div>
<div class="mdui-list-item-content">{{user.nickname}}</div>
<button class="btn" @click="refreshRoom">刷新房间状态</button>
<button class="btn" @click="closeRoom">关闭房间</button>
<h4 style="margin-top: 16px;">在线用户</h4>
<ul class="user-list">
<li v-for="user in roomInfo.roomUsers" :key="user.userId" class="user-item">
<img :src="user.avatarUrl" class="user-avatar" alt="avatar" />
<span class="user-name">{{user.nickname}}</span>
</li>
</ul>
<button v-if="roomInfo.roomId" @click="closeRoom">关闭房间</button>
</div>
</div>
<button @click="playTrack">播放</button>
<button @click="pauseTrack">暂停</button>
<button @click="seekTrack">同步进度</button>
<div class="section">
<h3>播放控制</h3>
<div class="control-buttons">
<button class="btn" @click="playTrack">播放</button>
<button class="btn" @click="pauseTrack">暂停</button>
<button class="btn" @click="seekTrack">同步进度</button>
</div>
</div>
<details>
<summary>播放列表</summary>
<br />
<div>
<span>歌单ID: </span><input v-model="playlistInfo.playlistId" />
<div class="section">
<div class="input-group">
<label>歌单ID:</label>
<input v-model="playlistInfo.playlistId" type="text" />
</div>
<button class="btn" @click="loadPlaylist">加载歌单</button>
<div style="margin-top: 12px; font-size: 14px; color: #555;">
歌单名称: {{playlistInfo.playlistName}}
</div>
<h4 style="margin-top: 16px;">歌单内容</h4>
<ul class="track-list">
<li
@click="gotoTrack(track.id)"
v-for="track in playlistInfo.playlistTracks"
:key="track.id"
class="track-item"
>
<img :src="track.al.picUrl" class="track-cover" alt="cover" />
<span class="track-name">{{track.name}}</span>
</li>
</ul>
</div>
<button @click="loadPlaylist">加载歌单到播放列表</button>
<span>{{playlistInfo.playlistName}}</span>
<br />
<br />
<div>歌单内容:</div>
<ul class="mdui-list">
<li
@click="gotoTrack(track.id)"
v-for="track in playlistInfo.playlistTracks"
class="mdui-list-item mdui-ripple"
>
<div class="mdui-list-item-avatar">
<img :src="track.al.picUrl" />
</div>
<div class="mdui-list-item-content">{{track.name}}</div>
</li>
</ul>
</details>
</body>
<script>
const logger = require('../util/logger.js')
PetiteVue.createApp({
message: '请点击获取登录状态',
account: {
@@ -127,7 +355,7 @@
this.account.userId = res.data.data.profile.userId
this.account.nickname = res.data.data.profile.nickname
this.account.login = true
this.message = '成功登录, 请创建房间'
this.message = '成功登录,请创建房间'
}
},
joinRoom: async function () {
@@ -140,7 +368,7 @@
cookie: localStorage.getItem('cookie'),
},
})
logger.info(res)
console.info(res)
if (res.data.code != 200) {
this.message = '加入房间出现问题: ' + res.data.message
} else {
@@ -153,7 +381,7 @@
cookie: localStorage.getItem('cookie'),
},
})
logger.info(res2)
console.info(res2)
const res3 = await axios({
url: 'listentogether/sync/playlist/get',
method: 'post',
@@ -174,7 +402,7 @@
cookie: localStorage.getItem('cookie'),
},
})
logger.info(resa)
console.info(resa)
this.playlistInfo.playlistTracks = resa.data.songs
}
},
@@ -186,7 +414,7 @@
cookie: localStorage.getItem('cookie'),
},
})
logger.info(res)
console.info(res)
if (res.data.code != 200) {
this.message = '创建房间出现问题: ' + res.data.message
} else {
@@ -201,7 +429,7 @@
cookie: localStorage.getItem('cookie'),
},
})
logger.info(res2)
console.info(res2)
}
},
refreshRoom: async function () {
@@ -211,9 +439,9 @@
cookie: localStorage.getItem('cookie'),
},
})
logger.info(res)
console.info(res)
if (res.data.code != 200 || !res.data.data.inRoom) {
this.message = '房间状态获取失败, 可能退出了房间'
this.message = '房间状态获取失败,可能退出了房间'
} else {
this.roomInfo.roomUsers = res.data.data.roomInfo.roomUsers
}
@@ -227,7 +455,7 @@
cookie: localStorage.getItem('cookie'),
},
})
logger.info(res)
console.info(res)
if (res.data.code != 200 || !res.data.data.success) {
this.message = '房间关闭失败'
} else {
@@ -244,7 +472,7 @@
cookie: localStorage.getItem('cookie'),
},
})
logger.info(res)
console.info(res)
this.playlistInfo.playlistName = res.data.playlist.name
this.playlistInfo.playlistTrackIds = res.data.playlist.trackIds
.map((track) => track.id)
@@ -257,7 +485,7 @@
cookie: localStorage.getItem('cookie'),
},
})
logger.info(resa)
console.info(resa)
this.playlistInfo.playlistTracks = resa.data.songs
if (this.roomInfo.roomId) {
const resb = await axios({
@@ -274,7 +502,7 @@
cookie: localStorage.getItem('cookie'),
},
})
logger.info(resb)
console.info(resb)
}
},
gotoTrack: async function (trackId) {
@@ -317,7 +545,7 @@
cookie: localStorage.getItem('cookie'),
},
})
logger.info(res)
console.info(res)
},
}).mount()
</script>
+190 -18
View File
@@ -5,21 +5,143 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录</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;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
max-width: 400px;
width: 100%;
text-align: center;
}
h1 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
color: #666;
margin-bottom: 32px;
}
.form-group {
margin-bottom: 20px;
text-align: left;
}
label {
display: block;
font-size: 14px;
font-weight: 500;
color: #555;
margin-bottom: 8px;
}
input {
width: 100%;
padding: 12px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 15px;
outline: none;
}
input:focus {
border-color: #333;
}
.btn {
width: 100%;
padding: 14px;
background: #333;
color: white;
font-size: 15px;
font-weight: 500;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
}
.btn:hover {
background: #555;
}
.btn:disabled {
background: #999;
cursor: not-allowed;
}
.result {
margin-top: 24px;
padding: 16px;
background: #f9f9f9;
border-radius: 6px;
font-size: 13px;
color: #666;
text-align: left;
white-space: pre-wrap;
word-break: break-all;
}
.error {
color: #ef4444;
background: #fef2f2;
}
.success {
color: #10b981;
background: #f0fdf4;
}
</style>
</head>
<body>
<div class="container">
<h1>登录</h1>
<p class="subtitle">使用手机号和密码登录网易云音乐</p>
<div class="form-group">
<label for="phone">手机号</label>
<input type="tel" id="phone" placeholder="请输入手机号" />
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" placeholder="请输入密码" />
</div>
<button id="loginBtn" class="btn" onclick="handleLogin()">登录</button>
<div id="result" class="result" style="display: none;"></div>
</div>
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
<script>
const phone = '' // 这里填手机号
const password = '' // 这里填密码
const fileUpdateTime = {}
if (!phone || !password) {
const msg = '请设置你的手机号码和密码'
alert(msg)
throw new Error(msg)
}
async function login() {
async function login(phone, password) {
const res = await axios({
url: `/login/cellphone`,
method: 'post',
@@ -30,17 +152,67 @@
})
return res.data.cookie
}
async function main() {
const cookieToken = await login()
const res = await axios({
url: `/login/status`,
method: 'post',
data: {
cookie: cookieToken,
},
})
async function handleLogin() {
const phoneInput = document.getElementById('phone')
const passwordInput = document.getElementById('password')
const loginBtn = document.getElementById('loginBtn')
const resultDiv = document.getElementById('result')
const phone = phoneInput.value.trim()
const password = passwordInput.value
if (!phone || !password) {
showResult('请输入手机号和密码', 'error')
return
}
loginBtn.disabled = true
loginBtn.textContent = '登录中...'
showResult('正在登录...', 'info')
try {
const cookieToken = await login(phone, password)
localStorage.setItem('cookie', cookieToken)
const res = await axios({
url: `/login/status`,
method: 'post',
data: {
cookie: cookieToken,
},
})
showResult(`登录成功!\n${JSON.stringify(res.data, null, 2)}`, 'success')
} catch (error) {
console.error('登录失败:', error)
const errorMsg = error.response?.data?.message || error.message || '登录失败,请重试'
showResult(`登录失败:${errorMsg}`, 'error')
} finally {
loginBtn.disabled = false
loginBtn.textContent = '登录'
}
}
main()
function showResult(message, type = 'info') {
const resultDiv = document.getElementById('result')
resultDiv.style.display = 'block'
resultDiv.textContent = message
resultDiv.className = 'result ' + type
}
// 支持回车登录
document.getElementById('password').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
handleLogin()
}
})
document.getElementById('phone').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
document.getElementById('password').focus()
}
})
</script>
</body>
+276 -29
View File
@@ -5,56 +5,297 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>歌单封面上传</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: 500px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 32px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
text-align: center;
}
h1 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
color: #666;
margin-bottom: 32px;
}
.login-link {
display: block;
margin-bottom: 24px;
color: #666;
font-size: 14px;
text-decoration: none;
}
.login-link:hover {
color: #333;
text-decoration: underline;
}
.cover-wrapper {
position: relative;
width: 180px;
height: 180px;
margin: 0 auto 24px;
}
.cover {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
border: 4px solid #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 20px;
text-align: left;
}
label {
display: block;
font-size: 14px;
font-weight: 500;
color: #555;
margin-bottom: 8px;
}
input[type="text"] {
width: 100%;
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
outline: none;
}
input[type="text"]:focus {
border-color: #333;
}
.upload-btn {
display: inline-block;
padding: 12px 28px;
background: #333;
color: white;
font-size: 15px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
border: none;
}
.upload-btn:hover {
background: #555;
}
.upload-btn input[type="file"] {
display: none;
}
.loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.loading.active {
opacity: 1;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid #e0e0e0;
border-top-color: #333;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.result {
margin-top: 20px;
padding: 12px 16px;
border-radius: 6px;
font-size: 14px;
text-align: left;
}
.result.success {
background: #d1fae5;
color: #065f46;
}
.result.error {
background: #fee2e2;
color: #991b1b;
}
</style>
</head>
<body>
<div>
<a href="/qrlogin-nocookie.html">
如果没登录,请先登录
</a>
<div class="container">
<h1>歌单封面上传</h1>
<p class="subtitle">上传自定义歌单封面图片</p>
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
<div class="form-group">
<label for="playlistId">歌单 ID</label>
<input type="text" id="playlistId" placeholder="请输入歌单ID" />
</div>
<div class="cover-wrapper">
<img id="playlist_cover" class="cover" src="" alt="歌单封面" />
<div class="loading" id="loading">
<div class="spinner"></div>
</div>
</div>
<label class="upload-btn">
选择封面图片
<input id="file" type="file" name="filename" accept="image/*" />
</label>
<div id="result" class="result" style="display: none;"></div>
</div>
<input id="file" type="file" name="filename" />
<img id="playlist_cover" style="height: 200px; width: 200px; border-radius: 50%" />
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
<script>
const playlist_id = ''
if (!playlist_id) {
const msg = '请设置你的歌单id'
alert(msg)
throw new Error(msg)
const loadingOverlay = document.getElementById('loading')
const playlistIdInput = document.getElementById('playlistId')
const resultDiv = document.getElementById('result')
function showLoading() {
loadingOverlay.classList.add('active')
}
main()
async function main() {
document.querySelector('input[type="file"]').addEventListener(
'change',
function (e) {
var file = this.files[0]
upload(file)
},
false,
)
const res = await axios({
url: `/playlist/detail?id=${playlist_id}&timestamp=${Date.now()}`,
function hideLoading() {
loadingOverlay.classList.remove('active')
}
function showResult(message, type) {
resultDiv.textContent = message
resultDiv.className = 'result ' + type
resultDiv.style.display = 'block'
}
function hideResult() {
resultDiv.style.display = 'none'
}
async function loadPlaylistCover() {
const playlistId = playlistIdInput.value.trim()
if (!playlistId) {
return
}
showLoading()
hideResult()
try {
const res = await axios({
url: `/playlist/detail?id=${playlistId}&timestamp=${Date.now()}`,
})
document.querySelector('#playlist_cover').src = res.data.playlist.coverImgUrl
hideResult()
} catch (error) {
console.error('加载封面失败:', error)
showResult('加载封面失败,请检查歌单ID', 'error')
} finally {
hideLoading()
}
}
// 监听歌单ID输入变化
playlistIdInput.addEventListener('input', function() {
loadPlaylistCover()
})
// 监听文件选择
document
.querySelector('input[type="file"]')
.addEventListener('change', async function (e) {
const file = this.files[0]
const playlistId = playlistIdInput.value.trim()
if (!playlistId) {
showResult('请先输入歌单ID', 'error')
return
}
if (!file) {
return
}
showLoading()
hideResult()
try {
await upload(file, playlistId)
} catch (error) {
console.error('上传失败:', error)
showResult('上传失败,请重试', 'error')
} finally {
hideLoading()
}
})
document.querySelector('#playlist_cover').src = res.data.playlist.coverImgUrl
}
async function upload(file) {
async function upload(file, playlistId) {
var formData = new FormData()
formData.append('imgFile', file)
const imgSize = await getImgSize(file)
const res = await axios({
method: 'post',
url: `/playlist/cover/update?id=${playlist_id}&cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width
}&imgX=0&imgY=0&timestamp=${Date.now()}`,
url: `/playlist/cover/update?id=${playlistId}&cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width}&imgX=0&imgY=0&timestamp=${Date.now()}`,
headers: {
'Content-Type': 'multipart/form-data',
},
data: formData,
})
document.querySelector('#playlist_cover').src = res.data.data.url
showResult('封面上传成功!', 'success')
}
function getImgSize(file) {
return new Promise((resolve, reject) => {
let reader = new FileReader()
@@ -68,10 +309,16 @@
height: this.height,
})
}
image.onerror = function() {
reject(new Error('图片加载失败'))
}
}
reader.onerror = function() {
reject(new Error('文件读取失败'))
}
})
}
</script>
</body>
</html>
</html>
+399 -244
View File
@@ -2,261 +2,416 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>歌单导入工具</title>
<!-- 引入Bootstrap CSS -->
<link href="https://fastly.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- 引入Bootstrap JS -->
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<!-- 引入axios用于发送异步请求 -->
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<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: 900px;
margin: 0 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;
}
.subtitle {
font-size: 14px;
color: #666;
margin-bottom: 24px;
}
.tabs {
display: flex;
gap: 4px;
margin-bottom: 24px;
border-bottom: 1px solid #eee;
}
.tab-btn {
padding: 12px 24px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
font-size: 14px;
font-weight: 500;
color: #666;
cursor: pointer;
transition: all 0.2s ease;
}
.tab-btn:hover {
color: #333;
}
.tab-btn.active {
color: #333;
border-bottom-color: #333;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
font-size: 14px;
font-weight: 500;
color: #555;
margin-bottom: 8px;
}
input[type="text"], textarea {
width: 100%;
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
outline: none;
font-family: inherit;
}
input[type="text"]:focus, textarea:focus {
border-color: #333;
}
textarea {
min-height: 120px;
resize: vertical;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
}
table th, table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
table th {
font-size: 14px;
font-weight: 600;
color: #555;
background: #f9f9f9;
}
table td input {
width: 100%;
}
.btn {
padding: 10px 20px;
background: #333;
color: white;
font-size: 14px;
font-weight: 500;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
}
.btn:hover {
background: #555;
}
.btn-secondary {
background: #666;
}
.btn-secondary:hover {
background: #888;
}
.input-group {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.input-group input {
flex: 1;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 24px;
}
.checkbox-group input[type="checkbox"] {
cursor: pointer;
}
.checkbox-group label {
margin: 0;
cursor: pointer;
}
input:disabled {
background: #f5f5f5;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="container mt-5">
<h1 class="mb-4">歌单导入工具</h1>
<p>请选择一种导入方式并填写相关信息:</p>
<div class="container">
<h1>歌单导入工具</h1>
<p class="subtitle">请选择一种导入方式并填写相关信息</p>
<!-- 表单开始 -->
<form id="importForm" novalidate>
<!-- 选项卡导航 -->
<ul class="nav nav-tabs mb-3" id="importTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="metadata-tab" data-bs-toggle="tab" data-bs-target="#metadata" type="button" role="tab" aria-controls="metadata" aria-selected="true">元数据导入</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="text-tab" data-bs-toggle="tab" data-bs-target="#text" type="button" role="tab" aria-controls="text" aria-selected="false">文字导入</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="link-tab" data-bs-toggle="tab" data-bs-target="#link" type="button" role="tab" aria-controls="link" aria-selected="false">链接导入</button>
</li>
</ul>
<ul class="tabs" id="importTabs" role="tablist">
<li role="presentation">
<button class="tab-btn active" id="metadata-tab" data-bs-toggle="tab" data-bs-target="#metadata" type="button" role="tab" aria-controls="metadata" aria-selected="true">元数据导入</button>
</li>
<li role="presentation">
<button class="tab-btn" id="text-tab" data-bs-toggle="tab" data-bs-target="#text" type="button" role="tab" aria-controls="text" aria-selected="false">文字导入</button>
</li>
<li role="presentation">
<button class="tab-btn" id="link-tab" data-bs-toggle="tab" data-bs-target="#link" type="button" role="tab" aria-controls="link" aria-selected="false">链接导入</button>
</li>
</ul>
<!-- 选项卡面板 -->
<div class="tab-content" id="importTabContent">
<!-- 元数据导入 -->
<div class="tab-pane fade show active" id="metadata" role="tabpanel" aria-labelledby="metadata-tab">
<table class="table table-bordered mb-3">
<thead>
<tr>
<th scope="col">歌曲名称</th>
<th scope="col">艺术家</th>
<th scope="col">专辑</th>
</tr>
</thead>
<tbody id="metadataTableBody">
<!-- 默认添加一行 -->
<tr>
<td><input type="text" class="form-control" name="name[]" placeholder="歌曲名称"></td>
<td><input type="text" class="form-control" name="artist[]" placeholder="艺术家"></td>
<td><input type="text" class="form-control" name="album[]" placeholder="专辑"></td>
</tr>
</tbody>
</table>
<button type="button" class="btn btn-secondary mb-3" id="addMetadataRow">增加歌曲</button>
<div class="tab-content active" id="importTabContent">
<!-- 元数据导入 -->
<div class="tab-content active" id="metadata" role="tabpanel" aria-labelledby="metadata-tab">
<table>
<thead>
<tr>
<th style="width: 33%">歌曲名称</th>
<th style="width: 33%">艺术家</th>
<th style="width: 33%">专辑</th>
</tr>
</thead>
<tbody id="metadataTableBody">
<tr>
<td><input type="text" name="name[]" placeholder="歌曲名称"></td>
<td><input type="text" name="artist[]" placeholder="艺术家"></td>
<td><input type="text" name="album[]" placeholder="专辑"></td>
</tr>
</tbody>
</table>
<button type="button" class="btn btn-secondary" id="addMetadataRow">增加歌曲</button>
</div>
<!-- 文字导入 -->
<div class="tab-content" id="text" role="tabpanel" aria-labelledby="text-tab">
<div class="form-group">
<label for="textInput">文字内容</label>
<textarea id="textInput" name="text" rows="5" placeholder="请输入歌曲信息,每行一首歌曲"></textarea>
</div>
<!-- 文字导入 -->
<div class="tab-pane fade" id="text" role="tabpanel" aria-labelledby="text-tab">
<div class="mb-3">
<label for="textInput" class="form-label">文字:</label>
<textarea class="form-control" id="textInput" name="text" rows="5"></textarea>
</div>
<div class="mb-3">
<label for="playlistNameInput" class="form-label">歌单名:</label>
<input type="text" class="form-control" id="playlistNameInput" name="playlistName" placeholder="请输入歌单名">
</div>
</div>
<!-- 链接导入 -->
<div class="tab-pane fade" id="link" role="tabpanel" aria-labelledby="link-tab">
<div class="mb-3">
<label for="linkInputs" class="form-label">链接:</label>
<div id="linkInputsContainer">
<div class="input-group mb-3">
<input type="text" class="form-control" id="linkInput0" name="linkInput0" placeholder="请输入链接">
<button type="button" class="btn btn-secondary removeLinkButton" data-index="0">×</button>
</div>
</div>
<button type="button" class="btn btn-secondary mb-3" id="addLinkButton">增加链接</button>
<div class="mb-3">
<label for="playlistNameLinkInput" class="form-label">歌单名:</label>
<input type="text" class="form-control" id="playlistNameLinkInput" name="playlistName" placeholder="请输入歌单名">
</div>
</div>
<div class="form-group">
<label for="playlistNameInput">歌单名称</label>
<input type="text" id="playlistNameInput" name="playlistName" placeholder="请输入歌单名">
</div>
</div>
<!-- 是否导入我喜欢的音乐 -->
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="importStarCheckbox">
<label class="form-check-label" for="importStarCheckbox">
导入“我喜欢的音乐”
</label>
<!-- 链接导入 -->
<div class="tab-content" id="link" role="tabpanel" aria-labelledby="link-tab">
<div class="form-group">
<label>链接列表</label>
<div id="linkInputsContainer">
<div class="input-group">
<input type="text" id="linkInput0" name="linkInput0" placeholder="请输入链接">
<button type="button" class="btn btn-secondary removeLinkButton" data-index="0">×</button>
</div>
</div>
<button type="button" class="btn btn-secondary" id="addLinkButton" style="margin-top: 8px;">增加链接</button>
</div>
<div class="form-group" style="margin-top: 20px;">
<label for="playlistNameLinkInput">歌单名称</label>
<input type="text" id="playlistNameLinkInput" name="playlistName" placeholder="请输入歌单名">
</div>
</div>
</div>
<!-- 提交按钮 -->
<button type="submit" class="btn btn-primary mt-3">导入歌曲</button>
</form>
<!-- 表单结束 -->
<div class="checkbox-group">
<input type="checkbox" value="" id="importStarCheckbox">
<label for="importStarCheckbox">
导入"我喜欢的音乐"
</label>
</div>
<script>
// 动态增加链接输入框
document.getElementById('addLinkButton').addEventListener('click', function() {
var container = document.getElementById('linkInputsContainer');
var newIndex = container.childElementCount - 1; // 减去非输入框元素的数量
var newInput = document.createElement('input');
newInput.type = 'text';
newInput.className = 'form-control';
newInput.id = `linkInput${newIndex}`;
newInput.name = `linkInput${newIndex}`;
newInput.placeholder = '请输入链接';
var removeButton = document.createElement('button');
removeButton.type = 'button';
removeButton.className = 'btn btn-secondary removeLinkButton';
removeButton.textContent = '×';
removeButton.dataset.index = newIndex.toString();
removeButton.addEventListener('click', function() {
var group = this.closest('.input-group');
container.removeChild(group);
});
var inputGroup = document.createElement('div');
inputGroup.className = 'input-group mb-3';
inputGroup.appendChild(newInput);
inputGroup.appendChild(removeButton);
container.appendChild(inputGroup);
});
// 动态增加元数据行
document.getElementById('addMetadataRow').addEventListener('click', function() {
var container = document.getElementById('metadataTableBody');
var newRow = document.createElement('tr');
var nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.className = 'form-control';
nameInput.name = 'name[]';
nameInput.placeholder = '歌曲名称';
var artistInput = document.createElement('input');
artistInput.type = 'text';
artistInput.className = 'form-control';
artistInput.name = 'artist[]';
artistInput.placeholder = '艺术家';
var albumInput = document.createElement('input');
albumInput.type = 'text';
albumInput.className = 'form-control';
albumInput.name = 'album[]';
albumInput.placeholder = '专辑';
newRow.innerHTML = `
<td>${nameInput.outerHTML}</td>
<td>${artistInput.outerHTML}</td>
<td>${albumInput.outerHTML}</td>
`;
container.appendChild(newRow);
});
document.getElementById('importForm').addEventListener('submit', async function(event) {
// 阻止默认行为
event.preventDefault();
// 获取表单值
let text = document.getElementById('textInput').value;
let links = [];
let local = [];
let playlistName = '';
// 获取所有链接输入框的值
let linkInputs = document.querySelectorAll('#linkInputsContainer .input-group .form-control');
linkInputs.forEach(function(input) {
if (input.value.trim() !== '') {
links.push(input.value);
}
});
// 获取元数据
let metadataRows = document.querySelectorAll('#metadataTableBody tr');
metadataRows.forEach(function(row) {
let name = row.querySelector('input[name="name[]"]').value;
let artist = row.querySelector('input[name="artist[]"]').value;
let album = row.querySelector('input[name="album[]"]').value;
if (name && artist && album) {
local.push({ name, artist, album });
}
});
// 检查是否有且只有一个输入字段被填写
let filledCount = (text ? 1 : 0) + (links.length > 0 ? 1 : 0) + (local.length > 0 ? 1 : 0);
if (filledCount !== 1) {
alert("请确保仅填写了一个输入字段!");
return;
}
// 获取歌单名
if (document.getElementById('importStarCheckbox').checked) {
playlistName = '我喜欢的音乐';
} else {
playlistName = document.getElementById('playlistNameInput').value ||
document.getElementById('playlistNameLinkInput').value ||
'导入音乐 ' + new Date().toLocaleString();
}
// 创建请求参数
let data = {};
if (text) {
data.text = text;
data.playlistName = playlistName;
} else if (links.length > 0) {
data.link = JSON.stringify(links);
data.playlistName = playlistName;
} else if (local.length > 0) {
data.local = JSON.stringify(local);
}
// 添加额外参数
if (document.getElementById('importStarCheckbox').checked) {
data.importStarPlaylist = true;
}
try {
const res = await axios({
url: `/playlist/import/name/task/create?timestamp=${Date.now()}`,
method: 'post',
data: data,
});
let taskId = res.data?.data?.taskId
if (taskId) {
alert(`任务创建成功! 正在导入, 请稍等; 任务id:${taskId}`)
// const res2 = await axios({
// url: `/playlist/import/task/status?timestamp=${Date.now()}`,
// method: 'post',
// data: {
// id: taskId
// },
// });
// alert(JSON.stringify(res2.data, null, 2));
}
} catch (error) {
console.error('Error:', error);
alert('导入失败,请检查您的输入或稍后再试。');
}
});
// 监听复选框状态变化
document.getElementById('importStarCheckbox').addEventListener('change', function() {
let isChecked = this.checked;
let playlistNameInputs = document.querySelectorAll('[name="playlistName"]');
playlistNameInputs.forEach(function(input) {
input.disabled = isChecked;
});
});
// 初始化时设置歌单名输入框的状态
document.getElementById('importStarCheckbox').dispatchEvent(new Event('change'));
</script>
<button type="submit" class="btn" id="submitBtn">导入歌曲</button>
</div>
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// 选项卡切换
const tabBtns = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content[id]');
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
tabBtns.forEach(b => b.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
btn.classList.add('active');
const targetId = btn.getAttribute('data-bs-target');
document.getElementById(targetId).classList.add('active');
});
});
// 动态增加链接输入框
document.getElementById('addLinkButton').addEventListener('click', function() {
var container = document.getElementById('linkInputsContainer');
var newIndex = container.children.length;
var newInput = document.createElement('input');
newInput.type = 'text';
newInput.className = '';
newInput.id = `linkInput${newIndex}`;
newInput.name = `linkInput${newIndex}`;
newInput.placeholder = '请输入链接';
newInput.style.cssText = 'flex: 1; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;';
var removeButton = document.createElement('button');
removeButton.type = 'button';
removeButton.className = 'btn btn-secondary';
removeButton.textContent = '×';
removeButton.dataset.index = newIndex.toString();
removeButton.addEventListener('click', function() {
var group = this.closest('.input-group');
container.removeChild(group);
});
var inputGroup = document.createElement('div');
inputGroup.className = 'input-group';
inputGroup.appendChild(newInput);
inputGroup.appendChild(removeButton);
container.appendChild(inputGroup);
});
// 动态增加元数据行
document.getElementById('addMetadataRow').addEventListener('click', function() {
var container = document.getElementById('metadataTableBody');
var newRow = document.createElement('tr');
newRow.innerHTML = `
<td><input type="text" name="name[]" placeholder="歌曲名称" style="width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;"></td>
<td><input type="text" name="artist[]" placeholder="艺术家" style="width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;"></td>
<td><input type="text" name="album[]" placeholder="专辑" style="width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;"></td>
`;
container.appendChild(newRow);
});
document.getElementById('submitBtn').addEventListener('click', async function() {
// 获取表单值
let text = document.getElementById('textInput').value;
let links = [];
let local = [];
let playlistName = '';
// 获取所有链接输入框的值
let linkInputs = document.querySelectorAll('#linkInputsContainer .input-group input[type="text"]');
linkInputs.forEach(function(input) {
if (input.value.trim() !== '') {
links.push(input.value);
}
});
// 获取元数据
let metadataRows = document.querySelectorAll('#metadataTableBody tr');
metadataRows.forEach(function(row) {
let name = row.querySelector('input[name="name[]"]').value;
let artist = row.querySelector('input[name="artist[]"]').value;
let album = row.querySelector('input[name="album[]"]').value;
if (name && artist && album) {
local.push({ name, artist, album });
}
});
// 检查是否有且只有一个输入字段被填写
let filledCount = (text ? 1 : 0) + (links.length > 0 ? 1 : 0) + (local.length > 0 ? 1 : 0);
if (filledCount !== 1) {
alert("请确保仅填写了一个输入字段!");
return;
}
// 获取歌单名
if (document.getElementById('importStarCheckbox').checked) {
playlistName = '我喜欢的音乐';
} else {
playlistName = document.getElementById('playlistNameInput').value ||
document.getElementById('playlistNameLinkInput').value ||
'导入音乐 ' + new Date().toLocaleString();
}
// 创建请求参数
let data = {};
if (text) {
data.text = text;
data.playlistName = playlistName;
} else if (links.length > 0) {
data.link = JSON.stringify(links);
data.playlistName = playlistName;
} else if (local.length > 0) {
data.local = JSON.stringify(local);
}
// 添加额外参数
if (document.getElementById('importStarCheckbox').checked) {
data.importStarPlaylist = true;
}
try {
const res = await axios({
url: `/playlist/import/name/task/create?timestamp=${Date.now()}`,
method: 'post',
data: data,
});
let taskId = res.data?.data?.taskId
if (taskId) {
alert(`任务创建成功!正在导入,请稍等;任务id:${taskId}`)
}
} catch (error) {
console.error('Error:', error);
alert('导入失败,请检查您的输入或稍后再试。');
}
});
// 监听复选框状态变化
document.getElementById('importStarCheckbox').addEventListener('change', function() {
let isChecked = this.checked;
let playlistNameInputs = document.querySelectorAll('[name="playlistName"]');
playlistNameInputs.forEach(function(input) {
input.disabled = isChecked;
});
});
// 初始化时设置歌单名输入框的状态
document.getElementById('importStarCheckbox').dispatchEvent(new Event('change'));
</script>
</body>
</html>

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