@neteasecloudmusicapienhanced/api 4.30.1 → 4.30.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.MD +48 -23
- package/data/china_ip_ranges.txt +4147 -0
- package/module/cloud.js +136 -119
- package/module/cloud_upload_complete.js +72 -0
- package/module/cloud_upload_token.js +111 -0
- package/module/comment_add.js +15 -0
- package/module/comment_delete.js +10 -0
- package/module/comment_info_list.js +30 -0
- package/module/comment_reply.js +13 -0
- package/module/search_suggest_pc.js +13 -0
- package/module/song_like.js +12 -0
- package/module/song_url_ncmget.js +2 -74
- package/module/song_url_v1_302.js +53 -0
- package/module/user_followeds.js +1 -1
- package/module/voice_upload.js +36 -22
- package/module/voicelist_my_created.js +13 -0
- package/module/voicelist_search.js +6 -9
- package/package.json +19 -25
- package/plugins/songUpload.js +67 -19
- package/plugins/upload.js +5 -7
- package/public/cloud.html +406 -39
- package/public/docs/home.md +232 -9
- package/public/docs/index.html +1 -1
- package/public/docs/logo.svg +6 -0
- package/public/docs/netease.png +0 -0
- package/public/index.html +29 -4
- package/public/static/docs.png +0 -0
- package/server.js +43 -18
- package/util/fileHelper.js +88 -0
- package/util/index.js +55 -52
- package/data/ChineseIPGenerate.csv +0 -26
- package/public/docs/ncmapireborn.png +0 -0
- /package/public/static/{screenshot1.png → module_test.png} +0 -0
package/public/docs/home.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
网易云音乐 NodeJS API Enhanced
|
|
4
4
|
|
|
5
|
+
最后更新于: 2026.2.15
|
|
6
|
+
|
|
5
7
|
## 灵感来自
|
|
6
8
|
|
|
7
9
|
[disoul/electron-cloud-music](https://github.com/disoul/electron-cloud-music)
|
|
@@ -781,7 +783,7 @@ tags: 歌单标签
|
|
|
781
783
|
**必选参数 :** `uid` : 用户 id
|
|
782
784
|
|
|
783
785
|
**可选参数 :**
|
|
784
|
-
`limit` : 返回数量 , 默认为
|
|
786
|
+
`limit` : 返回数量 , 默认为 20
|
|
785
787
|
|
|
786
788
|
`offset` : 偏移数量,用于分页 ,如 :( 页数 -1)\*30, 其中 30 为 limit 的值 , 默认为 0
|
|
787
789
|
|
|
@@ -1252,6 +1254,21 @@ tags: 歌单标签
|
|
|
1252
1254
|
|
|
1253
1255
|
说明:`杜比全景声`音质需要设备支持,不同的设备可能会返回不同码率的 url。cookie 需要传入`os=pc`保证返回正常码率的 url。
|
|
1254
1256
|
|
|
1257
|
+
### 302到音乐 url - 新版
|
|
1258
|
+
|
|
1259
|
+
说明 : 只允许传入单个`id`,会使用302重定向请求到目标url
|
|
1260
|
+
|
|
1261
|
+
**必选参数 :** `id` : 音乐 id
|
|
1262
|
+
`level`: 播放音质等级, 分为 `standard` => `标准`,`higher` => `较高`, `exhigh`=>`极高`,
|
|
1263
|
+
`lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清环绕声`, `sky` => `沉浸环绕声`, `dolby` => `杜比全景声`, `jymaster` => `超清母带`
|
|
1264
|
+
`unblock`: 是否使用使用歌曲解锁, 分为`true`和`false`
|
|
1265
|
+
|
|
1266
|
+
**接口地址 :** `/song/url/v1/302`
|
|
1267
|
+
|
|
1268
|
+
**调用例子 :** `/song/url/v1/302?id=1969519579&level=exhigh`
|
|
1269
|
+
|
|
1270
|
+
说明:`杜比全景声`音质需要设备支持,不同的设备可能会返回不同码率的 url。cookie 需要传入`os=pc`保证返回正常码率的 url。
|
|
1271
|
+
|
|
1255
1272
|
### 音乐是否可用
|
|
1256
1273
|
|
|
1257
1274
|
说明: 调用此接口,传入歌曲 id, 可获取音乐是否可用,返回 `{ success: true, message: 'ok' }` 或者 `{ success: false, message: '亲爱的,暂无版权' }`
|
|
@@ -1684,6 +1701,62 @@ tags: 歌单标签
|
|
|
1684
1701
|
|
|
1685
1702
|
**调用例子 :** `/comment/video?id=89ADDE33C0AAE8EC14B99F6750DB954D`
|
|
1686
1703
|
|
|
1704
|
+
### 评论统计数据
|
|
1705
|
+
|
|
1706
|
+
说明 : 调用此接口 , 传入资源类型和资源 id 列表 , 可批量获取对应资源的评论统计数据 ( 不需要登录 )
|
|
1707
|
+
|
|
1708
|
+
**必选参数 :**
|
|
1709
|
+
|
|
1710
|
+
`type`: 数字 , 资源类型 , 对应以下类型
|
|
1711
|
+
|
|
1712
|
+
```
|
|
1713
|
+
0: 歌曲
|
|
1714
|
+
|
|
1715
|
+
1: mv
|
|
1716
|
+
|
|
1717
|
+
2: 歌单
|
|
1718
|
+
|
|
1719
|
+
3: 专辑
|
|
1720
|
+
|
|
1721
|
+
4: 电台节目
|
|
1722
|
+
|
|
1723
|
+
5: 视频
|
|
1724
|
+
|
|
1725
|
+
6: 动态
|
|
1726
|
+
|
|
1727
|
+
7: 电台
|
|
1728
|
+
```
|
|
1729
|
+
|
|
1730
|
+
`ids`: 资源 id 列表 , 多个 id 用逗号分隔 , 如 `186016,347230`
|
|
1731
|
+
|
|
1732
|
+
**接口地址 :** `/comment/info/list`
|
|
1733
|
+
|
|
1734
|
+
**调用例子 :** `/comment/info/list?type=0&ids=186016,347230`
|
|
1735
|
+
|
|
1736
|
+
**返回数据 :**
|
|
1737
|
+
|
|
1738
|
+
```json
|
|
1739
|
+
{
|
|
1740
|
+
"data": [
|
|
1741
|
+
{
|
|
1742
|
+
"latestLikedUsers": null,
|
|
1743
|
+
"liked": false,
|
|
1744
|
+
"comments": null,
|
|
1745
|
+
"resourceType": 4,
|
|
1746
|
+
"resourceId": 186016,
|
|
1747
|
+
"commentUpgraded": false,
|
|
1748
|
+
"musicianSaidCount": 0,
|
|
1749
|
+
"commentCountDesc": "100w+",
|
|
1750
|
+
"likedCount": 347,
|
|
1751
|
+
"commentCount": 1970844,
|
|
1752
|
+
"shareCount": 109721,
|
|
1753
|
+
"threadId": "R_SO_4_186016"
|
|
1754
|
+
}
|
|
1755
|
+
],
|
|
1756
|
+
"code": 200
|
|
1757
|
+
}
|
|
1758
|
+
```
|
|
1759
|
+
|
|
1687
1760
|
### 热门评论
|
|
1688
1761
|
|
|
1689
1762
|
说明 : 调用此接口 , 传入 type, 资源 id 可获得对应资源热门评论 ( 不需要登录 )
|
|
@@ -2766,7 +2839,7 @@ type : 地区
|
|
|
2766
2839
|
|
|
2767
2840
|
参考: https://github.com/neteasecloudmusicapienhanced/api-enhanced/blob/main/public/cloud.html
|
|
2768
2841
|
|
|
2769
|
-
访问地址: http://localhost:3000/cloud.html
|
|
2842
|
+
访问地址: http://localhost:3000/cloud.html
|
|
2770
2843
|
|
|
2771
2844
|
支持命令行调用,参考 module_example 目录下`song_upload.js`
|
|
2772
2845
|
|
|
@@ -2774,6 +2847,78 @@ type : 地区
|
|
|
2774
2847
|
|
|
2775
2848
|
**调用例子 :** `/cloud`
|
|
2776
2849
|
|
|
2850
|
+
#### 上传模式说明
|
|
2851
|
+
|
|
2852
|
+
云盘上传支持两种模式:
|
|
2853
|
+
|
|
2854
|
+
**1. 后端代理模式 (默认)**
|
|
2855
|
+
|
|
2856
|
+
文件通过服务器转发到云存储,调用简单,但受服务器限制:
|
|
2857
|
+
|
|
2858
|
+
- Vercel Serverless Functions 限制请求体大小为 4.5MB
|
|
2859
|
+
- 自建服务器需配置足够大的请求体限制
|
|
2860
|
+
|
|
2861
|
+
**2. 客户端直传模式 (推荐用于 Vercel)**
|
|
2862
|
+
|
|
2863
|
+
文件直接从客户端上传到云存储服务器,绕过服务器限制:
|
|
2864
|
+
|
|
2865
|
+
- 支持大文件上传
|
|
2866
|
+
- 适合 Vercel、Netlify 等有请求体限制的平台
|
|
2867
|
+
- 需要前端配合实现
|
|
2868
|
+
|
|
2869
|
+
#### 客户端直传相关接口
|
|
2870
|
+
|
|
2871
|
+
**获取上传凭证**
|
|
2872
|
+
|
|
2873
|
+
**接口地址 :** `/cloud/upload/token`
|
|
2874
|
+
|
|
2875
|
+
**必选参数 :**
|
|
2876
|
+
|
|
2877
|
+
- `cookie`: 网易云音乐 Cookie (在请求体中传递)
|
|
2878
|
+
- `md5`: 文件 MD5 值
|
|
2879
|
+
- `fileSize`: 文件大小(字节)
|
|
2880
|
+
- `filename`: 文件名
|
|
2881
|
+
|
|
2882
|
+
**返回数据 :**
|
|
2883
|
+
|
|
2884
|
+
```json
|
|
2885
|
+
{
|
|
2886
|
+
"code": 200,
|
|
2887
|
+
"data": {
|
|
2888
|
+
"needUpload": true,
|
|
2889
|
+
"songId": "...",
|
|
2890
|
+
"uploadToken": "...",
|
|
2891
|
+
"uploadUrl": "...",
|
|
2892
|
+
"resourceId": "..."
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
```
|
|
2896
|
+
|
|
2897
|
+
**完成上传导入**
|
|
2898
|
+
|
|
2899
|
+
**接口地址 :** `/cloud/upload/complete`
|
|
2900
|
+
|
|
2901
|
+
**必选参数 :**
|
|
2902
|
+
|
|
2903
|
+
- `cookie`: 网易云音乐 Cookie (在请求体中传递)
|
|
2904
|
+
- `songId`: 歌曲 ID
|
|
2905
|
+
- `resourceId`: 资源 ID
|
|
2906
|
+
- `md5`: 文件 MD5
|
|
2907
|
+
- `filename`: 文件名
|
|
2908
|
+
|
|
2909
|
+
**可选参数 :**
|
|
2910
|
+
|
|
2911
|
+
- `song`: 歌曲名
|
|
2912
|
+
- `artist`: 艺术家
|
|
2913
|
+
- `album`: 专辑名
|
|
2914
|
+
|
|
2915
|
+
#### 客户端直传流程
|
|
2916
|
+
|
|
2917
|
+
1. 客户端计算文件 MD5
|
|
2918
|
+
2. 调用 `/cloud/upload/token` 获取上传凭证
|
|
2919
|
+
3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl`
|
|
2920
|
+
4. 调用 `/cloud/upload/complete` 完成导入
|
|
2921
|
+
|
|
2777
2922
|
### 云盘歌曲信息匹配纠正
|
|
2778
2923
|
|
|
2779
2924
|
说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0
|
|
@@ -2790,6 +2935,7 @@ type : 地区
|
|
|
2790
2935
|
**调用例子 :** `/cloud/match?uid=32953014&sid=aaa&asid=bbb` `/cloud/match?uid=32953014&sid=bbb&asid=0`
|
|
2791
2936
|
|
|
2792
2937
|
### 获取云盘歌词
|
|
2938
|
+
|
|
2793
2939
|
说明: 调用此接口, 获取云盘歌曲的歌词,歌词来自此文件的音乐元数据`LYRICS`标签。
|
|
2794
2940
|
|
|
2795
2941
|
**可选参数 :**
|
|
@@ -3972,13 +4118,15 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009`
|
|
|
3972
4118
|
|
|
3973
4119
|
**接口地址:** `/voicelist/search`
|
|
3974
4120
|
|
|
3975
|
-
|
|
4121
|
+
**必选参数:**
|
|
3976
4122
|
|
|
3977
|
-
`
|
|
4123
|
+
`keyword`: 搜索关键词
|
|
3978
4124
|
|
|
3979
|
-
|
|
4125
|
+
**可选参数:**
|
|
3980
4126
|
|
|
3981
|
-
`
|
|
4127
|
+
`limit`: 取出歌单数量, 默认为 10
|
|
4128
|
+
|
|
4129
|
+
`offset`: 偏移数量 , 用于分页 , 默认为 30
|
|
3982
4130
|
|
|
3983
4131
|
### 播客声音列表
|
|
3984
4132
|
|
|
@@ -4883,12 +5031,11 @@ let data = encodeURIComponent(
|
|
|
4883
5031
|
|
|
4884
5032
|
**调用例子:** `/vip/sign/info`
|
|
4885
5033
|
|
|
4886
|
-
|
|
4887
5034
|
### 用户的创建歌单列表
|
|
4888
5035
|
|
|
4889
5036
|
说明 : 调用此接口, 传入用户id, 获取用户的创建歌单列表
|
|
4890
5037
|
|
|
4891
|
-
**必选参数 :**
|
|
5038
|
+
**必选参数 :**
|
|
4892
5039
|
|
|
4893
5040
|
`uid`: 用户 id
|
|
4894
5041
|
|
|
@@ -4906,7 +5053,7 @@ let data = encodeURIComponent(
|
|
|
4906
5053
|
|
|
4907
5054
|
说明 : 调用此接口, 传入用户id, 获取用户的收藏歌单列表
|
|
4908
5055
|
|
|
4909
|
-
**必选参数 :**
|
|
5056
|
+
**必选参数 :**
|
|
4910
5057
|
|
|
4911
5058
|
`uid`: 用户 id
|
|
4912
5059
|
|
|
@@ -4920,6 +5067,82 @@ let data = encodeURIComponent(
|
|
|
4920
5067
|
|
|
4921
5068
|
**调用例子 :** `/user/playlist/collect?uid=32953014`
|
|
4922
5069
|
|
|
5070
|
+
### 搜索建议 - PC端
|
|
5071
|
+
|
|
5072
|
+
说明 : 调用此接口, 传入搜索关键词, 获取搜索建议
|
|
5073
|
+
|
|
5074
|
+
**必选参数 :**
|
|
5075
|
+
|
|
5076
|
+
`keyword`: 搜索关键词
|
|
5077
|
+
|
|
5078
|
+
**接口地址 :** `/search/suggest/pc`
|
|
5079
|
+
|
|
5080
|
+
**调用例子 :** `/search/suggest/pc?keyword=海阔天空`
|
|
5081
|
+
|
|
5082
|
+
### 喜欢歌曲 - 新版
|
|
5083
|
+
|
|
5084
|
+
说明 : 登录后调用此接口, 传入歌曲 id 用户id和喜欢状态, 可喜欢/取消喜欢歌曲
|
|
5085
|
+
|
|
5086
|
+
**必选参数 :**
|
|
5087
|
+
|
|
5088
|
+
`id`: 歌曲 id
|
|
5089
|
+
`uid`: 用户 id
|
|
5090
|
+
`like`: 喜欢状态, true 表示喜欢, false 表示取消喜欢
|
|
5091
|
+
|
|
5092
|
+
**接口地址 :** `/song/like`
|
|
5093
|
+
|
|
5094
|
+
**调用例子 :** `/song/like?id=2058263032&uid=32953014&like=true`
|
|
5095
|
+
|
|
5096
|
+
### 我创建的播客声音
|
|
5097
|
+
|
|
5098
|
+
说明 : 登录后调用此接口, 获取我创建的博客声音
|
|
5099
|
+
|
|
5100
|
+
**可选参数 :**
|
|
5101
|
+
|
|
5102
|
+
`limit` : 返回数量 , 默认为 20
|
|
5103
|
+
|
|
5104
|
+
**接口地址 :** `/voicelist/my/created`
|
|
5105
|
+
|
|
5106
|
+
**调用例子 :** `/voicelist/my/created`
|
|
5107
|
+
|
|
5108
|
+
### 发布评论
|
|
5109
|
+
|
|
5110
|
+
说明 : 登录后调用此接口, 传入评论线程 id, 评论内容等信息, 发布评论
|
|
5111
|
+
|
|
5112
|
+
**必选参数 :**
|
|
5113
|
+
|
|
5114
|
+
`id`: 歌曲id
|
|
5115
|
+
`content`: 评论内容
|
|
5116
|
+
|
|
5117
|
+
**接口地址 :** `/comment/add`
|
|
5118
|
+
|
|
5119
|
+
**调用例子 :** `/comment/add?id=2058263032&content=这首歌太棒了!`
|
|
5120
|
+
|
|
5121
|
+
### 删除评论
|
|
5122
|
+
|
|
5123
|
+
说明 : 登录后调用此接口, 传入评论 id, 删除评论
|
|
5124
|
+
|
|
5125
|
+
**必选参数 :**
|
|
5126
|
+
`cid`: 评论 id
|
|
5127
|
+
`id`: 歌曲id
|
|
5128
|
+
|
|
5129
|
+
**接口地址 :** `/comment/delete`
|
|
5130
|
+
|
|
5131
|
+
**调用例子 :** `/comment/delete?threadId=2058263032&commentId=123456789`
|
|
5132
|
+
|
|
5133
|
+
### 回复评论
|
|
5134
|
+
|
|
5135
|
+
说明 : 登录后调用此接口, 传入歌曲 id, 回复内容等信息, 回复评论
|
|
5136
|
+
|
|
5137
|
+
**必选参数 :**
|
|
5138
|
+
`id`: 歌曲id
|
|
5139
|
+
`commentId`: 被回复的评论 id
|
|
5140
|
+
`content`: 回复内容
|
|
5141
|
+
|
|
5142
|
+
**接口地址 :** `/comment/reply`
|
|
5143
|
+
|
|
5144
|
+
**调用例子 :** `/comment/reply?id=2058263032&commentId=123456789&content=我也觉得这首歌很棒!`
|
|
5145
|
+
|
|
4923
5146
|
## 离线访问此文档
|
|
4924
5147
|
|
|
4925
5148
|
此文档同时也是 Progressive Web Apps(PWA), 加入了 serviceWorker, 可离线访问
|
package/public/docs/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="KEYWords" contect="网易云音乐,网易云音乐 api,网易云音乐 nodejs,网易云音乐 node.js">
|
|
6
6
|
<meta name="description" content="网易云音乐 NodeJS API Enhanced">
|
|
7
7
|
<title>网易云音乐 NodeJS API Enhanced</title>
|
|
8
|
-
<link rel="icon" href="
|
|
8
|
+
<link rel="icon" href="netease.png">
|
|
9
9
|
<meta name="description" content="Description">
|
|
10
10
|
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
|
11
11
|
<meta name="referrer" content="never">
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
2
|
+
<g>
|
|
3
|
+
<path fill="none" d="M0 0h24v24H0z"/>
|
|
4
|
+
<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"/>
|
|
5
|
+
</g>
|
|
6
|
+
</svg>
|
|
Binary file
|
package/public/index.html
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
7
|
+
<link rel="icon" href="docs/netease.png">
|
|
7
8
|
<title>网易云音乐 API Enhanced</title>
|
|
8
9
|
<style>
|
|
9
10
|
:root {
|
|
@@ -18,20 +19,33 @@
|
|
|
18
19
|
html, body { height: 100%; }
|
|
19
20
|
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; }
|
|
20
21
|
.container { max-width: 960px; margin: 40px auto; padding: 0 20px; }
|
|
22
|
+
@media (max-width: 480px) {
|
|
23
|
+
.container { margin: 20px auto; padding: 0 16px; }
|
|
24
|
+
header.site-header h1 { font-size: 22px; }
|
|
25
|
+
.block { padding: 16px; }
|
|
26
|
+
}
|
|
21
27
|
header.site-header { margin-bottom: 24px; }
|
|
22
28
|
header.site-header h1 { font-size: 28px; font-weight: 600; margin: 0; }
|
|
23
29
|
.badge { display: inline-block; margin-left: 8px; padding: 4px 10px; border: 1px solid var(--border); border-radius: 12px; font-size: 12px; color: var(--muted); }
|
|
24
30
|
.sub { margin-top: 8px; color: var(--muted); font-size: 14px; }
|
|
25
31
|
.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); }
|
|
26
32
|
.block h2 { margin: 0 0 12px; font-size: 18px; font-weight: 600; }
|
|
27
|
-
.kvs { display: grid; grid-template-columns:
|
|
28
|
-
.kvs div:first-child { color: var(--muted); }
|
|
33
|
+
.kvs { display: grid; grid-template-columns: 100px 1fr; gap: 8px 12px; align-items: start; }
|
|
34
|
+
.kvs div:first-child { color: var(--muted); flex-shrink: 0; }
|
|
35
|
+
.kvs div:last-child { word-break: break-all; overflow-wrap: anywhere; min-width: 0; overflow: hidden; }
|
|
36
|
+
@media (max-width: 480px) {
|
|
37
|
+
.kvs { grid-template-columns: 1fr; gap: 4px 12px; }
|
|
38
|
+
.kvs div:first-child { font-weight: 500; }
|
|
39
|
+
}
|
|
29
40
|
ul.links { list-style: none; padding: 0; margin: 0; }
|
|
30
41
|
ul.links li { margin: 8px 0; }
|
|
31
42
|
ul.links a { color: var(--fg); text-decoration: none; border-bottom: 1px dotted var(--border); transition: all 0.2s ease; }
|
|
32
43
|
ul.links a:hover { color: var(--accent); border-bottom-color: var(--accent); }
|
|
33
|
-
pre { margin: 0; background: #f9f9f9; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow: auto; }
|
|
44
|
+
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; }
|
|
34
45
|
code { font-family: 'Courier New', monospace; font-size: 13px; }
|
|
46
|
+
@media (max-width: 480px) {
|
|
47
|
+
code { font-size: 12px; }
|
|
48
|
+
}
|
|
35
49
|
footer.site-footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); text-align: center; }
|
|
36
50
|
footer.site-footer a { color: var(--fg); text-decoration: none; transition: color 0.2s ease; }
|
|
37
51
|
footer.site-footer a:hover { color: var(--accent); }
|
|
@@ -71,7 +85,18 @@
|
|
|
71
85
|
<h2>调试部分</h2>
|
|
72
86
|
<pre><code>curl -s {origin}/inner/version
|
|
73
87
|
curl -s {origin}/search?keywords=网易云</code></pre>
|
|
74
|
-
<
|
|
88
|
+
<div style="margin-top:10px; line-height:2;">
|
|
89
|
+
<a href="/api.html">交互式调试</a> ·
|
|
90
|
+
<a href="/qrlogin.html">二维码登录示例</a> ·
|
|
91
|
+
<a href="/unblock_test.html">解灰测试</a> ·
|
|
92
|
+
<a href="/audio_match_demo/index.html">听歌识曲 Demo</a> ·
|
|
93
|
+
<a href="/cloud.html">云盘上传</a> ·
|
|
94
|
+
<a href="/playlist_import.html">歌单导入</a> ·
|
|
95
|
+
<a href="/eapi_decrypt.html">EAPI 解密</a> ·
|
|
96
|
+
<a href="/listen_together_host.html">一起听示例</a> ·
|
|
97
|
+
<a href="/playlist_cover_update.html">更新歌单封面示例</a> ·
|
|
98
|
+
<a href="/avatar_update.html">头像更新示例</a>
|
|
99
|
+
</div>
|
|
75
100
|
</section>
|
|
76
101
|
|
|
77
102
|
<footer class="site-footer">
|
package/public/static/docs.png
CHANGED
|
Binary file
|
package/server.js
CHANGED
|
@@ -178,10 +178,25 @@ async function consturctServer(moduleDefs) {
|
|
|
178
178
|
/**
|
|
179
179
|
* Body Parser and File Upload
|
|
180
180
|
*/
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
app.use(
|
|
181
|
+
const MAX_UPLOAD_SIZE_MB = 500
|
|
182
|
+
const MAX_UPLOAD_SIZE_BYTES = MAX_UPLOAD_SIZE_MB * 1024 * 1024
|
|
183
|
+
|
|
184
|
+
app.use(express.json({ limit: `${MAX_UPLOAD_SIZE_MB}mb` }))
|
|
185
|
+
app.use(
|
|
186
|
+
express.urlencoded({ extended: false, limit: `${MAX_UPLOAD_SIZE_MB}mb` }),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
app.use(
|
|
190
|
+
fileUpload({
|
|
191
|
+
limits: {
|
|
192
|
+
fileSize: MAX_UPLOAD_SIZE_BYTES,
|
|
193
|
+
},
|
|
194
|
+
useTempFiles: true,
|
|
195
|
+
tempFileDir: require('os').tmpdir(),
|
|
196
|
+
abortOnLimit: true,
|
|
197
|
+
parseNested: true,
|
|
198
|
+
}),
|
|
199
|
+
)
|
|
185
200
|
|
|
186
201
|
/**
|
|
187
202
|
* Cache
|
|
@@ -206,7 +221,7 @@ async function consturctServer(moduleDefs) {
|
|
|
206
221
|
|
|
207
222
|
for (const moduleDef of moduleDefinitions) {
|
|
208
223
|
// Register the route.
|
|
209
|
-
app.
|
|
224
|
+
app.all(moduleDef.route, async (req, res) => {
|
|
210
225
|
;[req.query, req.body].forEach((item) => {
|
|
211
226
|
// item may be undefined (some environments / middlewares).
|
|
212
227
|
// Guard access to avoid "Cannot read properties of undefined (reading 'cookie')".
|
|
@@ -227,25 +242,30 @@ async function consturctServer(moduleDefs) {
|
|
|
227
242
|
const moduleResponse = await moduleDef.module(query, (...params) => {
|
|
228
243
|
// 参数注入客户端IP
|
|
229
244
|
const obj = [...params]
|
|
230
|
-
|
|
245
|
+
const options = obj[2] || {}
|
|
246
|
+
if (!options.randomCNIP) {
|
|
247
|
+
let ip = req.ip
|
|
231
248
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
249
|
+
if (ip.substring(0, 7) == '::ffff:') {
|
|
250
|
+
ip = ip.substring(7)
|
|
251
|
+
}
|
|
252
|
+
if (ip == '::1') {
|
|
253
|
+
ip = global.cnIp
|
|
254
|
+
}
|
|
255
|
+
// logger.info('Requested from ip:', ip)
|
|
256
|
+
obj[2] = {
|
|
257
|
+
...options,
|
|
258
|
+
ip,
|
|
259
|
+
}
|
|
242
260
|
}
|
|
261
|
+
|
|
243
262
|
return request(...obj)
|
|
244
263
|
})
|
|
245
264
|
logger.info(`Request Success: ${decode(req.originalUrl)}`)
|
|
246
265
|
|
|
266
|
+
// 夹带私货部分:如果开启了通用解锁,并且是获取歌曲URL的接口,则尝试解锁(如果需要的话)ヾ(≧▽≦*)o
|
|
247
267
|
if (
|
|
248
|
-
|
|
268
|
+
req.baseUrl === '/song/url/v1' &&
|
|
249
269
|
process.env.ENABLE_GENERAL_UNBLOCK === 'true'
|
|
250
270
|
) {
|
|
251
271
|
const song = moduleResponse.body.data[0]
|
|
@@ -260,7 +280,7 @@ async function consturctServer(moduleDefs) {
|
|
|
260
280
|
logger.info('Starting unblock(uses general unblock):', req.query.id)
|
|
261
281
|
const result = await matchID(req.query.id)
|
|
262
282
|
song.url = result.data.url
|
|
263
|
-
song.freeTrialInfo =
|
|
283
|
+
song.freeTrialInfo = null
|
|
264
284
|
logger.info('Unblock success! url:', song.url)
|
|
265
285
|
}
|
|
266
286
|
if (song.url && song.url.includes('kuwo')) {
|
|
@@ -288,6 +308,11 @@ async function consturctServer(moduleDefs) {
|
|
|
288
308
|
}
|
|
289
309
|
}
|
|
290
310
|
}
|
|
311
|
+
if (moduleResponse.redirectUrl) {
|
|
312
|
+
res.redirect(moduleResponse.status || 302, moduleResponse.redirectUrl)
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
|
|
291
316
|
res.status(moduleResponse.status).send(moduleResponse.body)
|
|
292
317
|
} catch (/** @type {*} */ moduleResponse) {
|
|
293
318
|
logger.error(`${decode(req.originalUrl)}`, {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const crypto = require('crypto')
|
|
3
|
+
const logger = require('./logger')
|
|
4
|
+
|
|
5
|
+
function isTempFile(file) {
|
|
6
|
+
return !!(file && file.tempFilePath)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function getFileSize(file) {
|
|
10
|
+
if (isTempFile(file)) {
|
|
11
|
+
const stats = await fs.promises.stat(file.tempFilePath)
|
|
12
|
+
return stats.size
|
|
13
|
+
}
|
|
14
|
+
return file.data ? file.data.byteLength : file.size || 0
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function getFileMd5(file) {
|
|
18
|
+
if (file.md5) {
|
|
19
|
+
return file.md5
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (isTempFile(file)) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const hash = crypto.createHash('md5')
|
|
25
|
+
const stream = fs.createReadStream(file.tempFilePath)
|
|
26
|
+
stream.on('data', (chunk) => hash.update(chunk))
|
|
27
|
+
stream.on('end', () => resolve(hash.digest('hex')))
|
|
28
|
+
stream.on('error', reject)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (file.data) {
|
|
33
|
+
return crypto.createHash('md5').update(file.data).digest('hex')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
throw new Error('无法计算文件MD5: 缺少文件数据')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getUploadData(file) {
|
|
40
|
+
if (isTempFile(file)) {
|
|
41
|
+
return fs.createReadStream(file.tempFilePath)
|
|
42
|
+
}
|
|
43
|
+
return file.data
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function cleanupTempFile(filePath) {
|
|
47
|
+
if (!filePath) return
|
|
48
|
+
try {
|
|
49
|
+
await fs.promises.unlink(filePath)
|
|
50
|
+
} catch (e) {
|
|
51
|
+
logger.info('临时文件清理失败:', e.message)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function readFileChunk(filePath, offset, length) {
|
|
56
|
+
const fd = await fs.promises.open(filePath, 'r')
|
|
57
|
+
const buffer = Buffer.alloc(length)
|
|
58
|
+
await fd.read(buffer, 0, length, offset)
|
|
59
|
+
await fd.close()
|
|
60
|
+
return buffer
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getFileExtension(filename) {
|
|
64
|
+
if (!filename) return 'mp3'
|
|
65
|
+
if (filename.includes('.')) {
|
|
66
|
+
return filename.split('.').pop().toLowerCase()
|
|
67
|
+
}
|
|
68
|
+
return 'mp3'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sanitizeFilename(filename) {
|
|
72
|
+
if (!filename) return 'unknown'
|
|
73
|
+
return filename
|
|
74
|
+
.replace(/\.[^.]+$/, '')
|
|
75
|
+
.replace(/\s/g, '')
|
|
76
|
+
.replace(/\./g, '_')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
isTempFile,
|
|
81
|
+
getFileSize,
|
|
82
|
+
getFileMd5,
|
|
83
|
+
getUploadData,
|
|
84
|
+
cleanupTempFile,
|
|
85
|
+
readFileChunk,
|
|
86
|
+
getFileExtension,
|
|
87
|
+
sanitizeFilename,
|
|
88
|
+
}
|