@neteasecloudmusicapienhanced/api 4.29.21 → 4.30.0

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 CHANGED
@@ -183,6 +183,15 @@ pnpm test
183
183
 
184
184
  - 欢迎提交 PR、Issue 参与维护
185
185
 
186
+ ## 最近更新日志
187
+ ### 4.30.0 | 2026.02.06
188
+ - feat: 新增音乐人黑胶会员任务接口 `/musician/vip/tasks` (#95)
189
+ - feat: 自动构建: 添加Windows、Linux、macOS预编译二进制文件 (#88)
190
+ - fix: 修复模块未定义问题
191
+ - chore: 更新依赖项 (music-metadata: ^11.11.1 -> ^11.11.2, ansi-escapes: ^7.2.0 -> ^7.3.0, commander: ^14.0.2 -> ^14.0.3)
192
+ - chore: 更新GitHub Actions (checkout: v4 -> v6, setup-node: v4 -> v6, upload-artifact: v4 -> v6, download-artifact: v4 -> v7, github-script: v7 -> v8)
193
+ - refactor: 注释掉IP地址日志输出以提升隐私保护
194
+
186
195
  ### 致谢
187
196
 
188
197
  原作者 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 项目为本项目基础 (该项目在`npmjs`网站上仍持续维护, 但 github 仓库已不再更新)
package/interface.d.ts CHANGED
@@ -1715,6 +1715,8 @@ export function nickname_check(
1715
1715
 
1716
1716
  export function musician_tasks_new(params: RequestBaseConfig): Promise<Response>
1717
1717
 
1718
+ export function musician_vip_tasks(params: RequestBaseConfig): Promise<Response>
1719
+
1718
1720
  export function playlist_update_playcount(
1719
1721
  params: {
1720
1722
  id?: number | string
@@ -0,0 +1,11 @@
1
+ // 获取音乐人任务
2
+
3
+ const createOption = require('../util/option.js')
4
+ module.exports = (query, request) => {
5
+ const data = {}
6
+ return request(
7
+ `/api/nmusician/workbench/special/right/vip/info`,
8
+ data,
9
+ createOption(query, 'eapi'),
10
+ )
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neteasecloudmusicapienhanced/api",
3
- "version": "4.29.21",
3
+ "version": "4.30.0",
4
4
  "description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护",
5
5
  "scripts": {
6
6
  "dev": "nodemon app.js",
@@ -9,9 +9,9 @@
9
9
  "lint": "eslint \"**/*.{js,ts}\"",
10
10
  "lint-fix": "eslint --fix \"**/*.{js,ts}\"",
11
11
  "prepare": "husky install",
12
- "pkgwin": "pkg . -t node18-win-x64 -C GZip -o bin/app --no-bytecode",
13
- "pkglinux": "pkg . -t node18-linux-x64 -C GZip -o bin/app --no-bytecode",
14
- "pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o bin/app --no-bytecode"
12
+ "pkgwin": "pkg . -t node18-win-x64 -C GZip -o precompiled/app",
13
+ "pkglinux": "pkg . -t node18-linux-x64 -C GZip -o precompiled/app",
14
+ "pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o precompiled/app"
15
15
  },
16
16
  "bin": "./app.js",
17
17
  "pkg": {
@@ -66,14 +66,14 @@
66
66
  "data"
67
67
  ],
68
68
  "dependencies": {
69
- "@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.1.3",
70
- "axios": "^1.13.4",
69
+ "@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.2.2",
70
+ "axios": "^1.13.5",
71
71
  "crypto-js": "^4.2.0",
72
- "dotenv": "^17.2.3",
72
+ "dotenv": "^17.2.4",
73
73
  "express": "^5.2.1",
74
74
  "express-fileupload": "^1.5.2",
75
75
  "md5": "^2.3.0",
76
- "music-metadata": "^11.11.1",
76
+ "music-metadata": "^11.12.0",
77
77
  "node-forge": "^1.3.3",
78
78
  "pac-proxy-agent": "^7.2.0",
79
79
  "qrcode": "^1.5.4",
package/public/api.html CHANGED
@@ -5,82 +5,148 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>API 调试界面</title>
7
7
  <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
8
14
  body {
9
- font-family: Arial, sans-serif;
10
- margin: 20px;
11
- display: flex;
12
- flex-direction: column;
13
- min-height: 100vh;
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16
+ min-height: 100vh;
17
+ background: #f5f5f5;
18
+ padding: 20px;
14
19
  }
20
+
15
21
  .container {
16
- display: flex;
17
- flex-direction: column;
18
- flex-grow: 1;
22
+ max-width: 1200px;
23
+ margin: 0 auto;
24
+ background: white;
25
+ border-radius: 12px;
26
+ padding: 32px;
27
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
19
28
  }
29
+
30
+ h1 {
31
+ font-size: 24px;
32
+ font-weight: 600;
33
+ color: #333;
34
+ margin-bottom: 24px;
35
+ }
36
+
20
37
  form {
21
- display: flex;
22
- flex-direction: row;
23
- align-items: center;
24
- gap: 10px;
25
- margin-bottom: 10px;
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: 16px;
41
+ margin-bottom: 24px;
42
+ }
43
+
44
+ .form-row {
45
+ display: flex;
46
+ gap: 12px;
47
+ align-items: center;
48
+ }
49
+
50
+ label {
51
+ font-size: 14px;
52
+ font-weight: 500;
53
+ color: #555;
54
+ min-width: 80px;
55
+ }
56
+
57
+ input, select {
58
+ padding: 10px 14px;
59
+ border: 1px solid #ddd;
60
+ border-radius: 6px;
61
+ font-size: 14px;
62
+ flex: 1;
63
+ outline: none;
26
64
  }
27
- input, button {
28
- padding: 10px;
29
- box-sizing: border-box;
30
- flex: 1;
65
+
66
+ input:focus, select:focus {
67
+ border-color: #333;
31
68
  }
69
+
32
70
  button {
33
- background-color: #4CAF50;
34
- color: white;
35
- border: none;
36
- cursor: pointer;
71
+ background: #333;
72
+ color: white;
73
+ padding: 10px 24px;
74
+ border: none;
75
+ border-radius: 6px;
76
+ font-size: 14px;
77
+ font-weight: 500;
78
+ cursor: pointer;
79
+ transition: background 0.2s ease;
80
+ }
81
+
82
+ button:hover {
83
+ background: #555;
37
84
  }
85
+
38
86
  .data-result {
39
- display: flex;
40
- flex-direction: row;
41
- flex-grow: 1;
87
+ display: flex;
88
+ gap: 16px;
89
+ min-height: 400px;
42
90
  }
91
+
43
92
  .data-result > div {
44
- display: flex;
45
- flex-direction: column;
46
- flex-grow: 1;
47
- padding: 10px;
48
- box-sizing: border-box;
93
+ flex: 1;
94
+ display: flex;
95
+ flex-direction: column;
49
96
  }
97
+
50
98
  .data-result label {
51
- margin-bottom: 10px;
99
+ margin-bottom: 8px;
100
+ padding: 0;
52
101
  }
53
- #data, #result {
54
- height: 100%;
55
- box-sizing: border-box;
102
+
103
+ textarea {
104
+ flex: 1;
105
+ padding: 12px;
106
+ border: 1px solid #ddd;
107
+ border-radius: 6px;
108
+ font-family: 'Courier New', monospace;
109
+ font-size: 13px;
110
+ resize: vertical;
111
+ min-height: 350px;
112
+ outline: none;
56
113
  }
57
- #data {
58
- border-right: 1px solid #ccc;
114
+
115
+ textarea:focus {
116
+ border-color: #333;
59
117
  }
60
118
  </style>
61
119
  </head>
62
120
  <body>
63
121
  <div class="container">
122
+ <h1>API 调试界面</h1>
64
123
  <form onsubmit="event.preventDefault(); sendRequest();">
65
- <label for="uri">uri</label>
66
- <input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
67
- <label for="crypto">crypto</label>
68
- <select id="crypto" name="crypto">
69
- <option value="weapi">weapi</option>
70
- <option value="eapi">eapi</option>
71
- <option value="api">api</option>
72
- <option value="linuxapi">linuxapi</option>
73
- <option value="" selected>(默认)</option>
74
- </select>
75
- <button type="submit">发送</button>
124
+ <div class="form-row">
125
+ <label for="uri">URI</label>
126
+ <input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
127
+ </div>
128
+ <div class="form-row">
129
+ <label for="crypto">加密方式</label>
130
+ <select id="crypto" name="crypto">
131
+ <option value="weapi">weapi</option>
132
+ <option value="eapi">eapi</option>
133
+ <option value="api">api</option>
134
+ <option value="linuxapi">linuxapi</option>
135
+ <option value="" selected>(默认)</option>
136
+ </select>
137
+ </div>
138
+ <div class="form-row">
139
+ <label></label>
140
+ <button type="submit">发送请求</button>
141
+ </div>
76
142
  </form>
77
143
  <div class="data-result">
78
144
  <div>
79
- <label for="result">result</label>
80
- <textarea id="result" name="result"></textarea>
145
+ <label for="result">响应结果</label>
146
+ <textarea id="result" name="result" readonly></textarea>
81
147
  </div>
82
148
  <div>
83
- <label for="data">data</label>
149
+ <label for="data">请求数据</label>
84
150
  <textarea id="data" name="data">
85
151
  {
86
152
  "cp": false,
@@ -1,6 +1,5 @@
1
1
  'use strict'
2
2
  const WASM_BINARY_PLACEHOLDER = 'WASM_BINARY_PLACEHOLDER';
3
- const logger = require('../../util/logger.js')
4
3
  // See https://github.com/Distributive-Network/PythonMonkey/issues/266
5
4
  if (typeof globalThis.setInterval != 'function'){
6
5
  globalThis.setInterval = function pm$$setInterval(fn, timeout) {
@@ -1612,9 +1611,9 @@ function instantiateRuntime(){
1612
1611
 
1613
1612
  function GenerateFP(floatArray) {
1614
1613
  let PCMBuffer = Float32Array.from(floatArray)
1615
- logger.info('[afp] input samples n=', PCMBuffer.length)
1614
+ console.info('[afp] input samples n=', PCMBuffer.length)
1616
1615
  return instantiateRuntime().then((fpRuntime) => {
1617
- logger.info('[afp] begin fingerprinting')
1616
+ console.info('[afp] begin fingerprinting')
1618
1617
  let fp_vector = fpRuntime.ExtractQueryFP(PCMBuffer.buffer)
1619
1618
  let result_buf = new Uint8Array(fp_vector.size());
1620
1619
  for (let t = 0; t < fp_vector.size(); t++)
@@ -1,27 +1,142 @@
1
1
  <!DOCTYPE html>
2
2
 
3
3
  <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>听歌识曲 Demo</title>
4
7
  <style>
5
8
  * {
6
- font-family: sans-serif;
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
7
12
  }
8
13
 
9
- pre {
10
- font-family: monospace;
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16
+ min-height: 100vh;
17
+ background: #f5f5f5;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 800px;
23
+ margin: 0 auto;
24
+ background: white;
25
+ border-radius: 12px;
26
+ padding: 32px;
27
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
28
+ }
29
+
30
+ h1 {
31
+ font-size: 24px;
32
+ font-weight: 600;
33
+ color: #333;
34
+ margin-bottom: 8px;
35
+ }
36
+
37
+ .subtitle {
38
+ font-size: 13px;
39
+ color: #666;
40
+ margin-bottom: 24px;
41
+ }
42
+
43
+ hr {
44
+ border: none;
45
+ border-top: 1px solid #eee;
46
+ margin: 20px 0;
47
+ }
48
+
49
+ p {
50
+ font-size: 14px;
51
+ color: #555;
52
+ line-height: 1.6;
53
+ margin-bottom: 12px;
11
54
  }
12
55
 
13
56
  a {
14
- font-family: sans-serif;
57
+ color: #333;
58
+ text-decoration: none;
59
+ }
60
+
61
+ a:hover {
62
+ text-decoration: underline;
63
+ }
64
+
65
+ .section {
66
+ margin-bottom: 24px;
67
+ }
68
+
69
+ .section h3 {
70
+ font-size: 16px;
71
+ font-weight: 600;
72
+ color: #333;
73
+ margin-bottom: 12px;
74
+ }
75
+
76
+ .control-group {
77
+ display: flex;
78
+ gap: 12px;
79
+ flex-wrap: wrap;
80
+ align-items: center;
81
+ margin-bottom: 16px;
82
+ }
83
+
84
+ button {
85
+ padding: 10px 20px;
86
+ background: #333;
87
+ color: white;
88
+ font-size: 14px;
89
+ font-weight: 500;
90
+ border: none;
91
+ border-radius: 6px;
92
+ cursor: pointer;
93
+ transition: background 0.2s ease;
94
+ }
95
+
96
+ button:hover {
97
+ background: #555;
98
+ }
99
+
100
+ button:disabled {
101
+ background: #999;
102
+ cursor: not-allowed;
103
+ }
104
+
105
+ input[type="file"] {
106
+ padding: 10px 14px;
107
+ border: 1px solid #ddd;
108
+ border-radius: 6px;
109
+ font-size: 14px;
110
+ }
111
+
112
+ .checkbox-group {
113
+ display: flex;
114
+ align-items: center;
115
+ gap: 8px;
116
+ }
117
+
118
+ .checkbox-group input[type="checkbox"] {
119
+ cursor: pointer;
120
+ }
121
+
122
+ .checkbox-group label {
123
+ margin: 0;
124
+ cursor: pointer;
125
+ font-size: 14px;
126
+ color: #555;
15
127
  }
16
128
 
17
129
  audio {
18
130
  width: 100%;
131
+ margin-bottom: 16px;
19
132
  }
20
133
 
21
134
  canvas {
22
135
  width: 100%;
23
136
  height: 0;
24
137
  transition: all linear 0.1s;
138
+ background: #f9f9f9;
139
+ border-radius: 6px;
25
140
  }
26
141
 
27
142
  .canvas-active {
@@ -29,39 +144,80 @@
29
144
  }
30
145
 
31
146
  pre {
32
- overflow: scroll;
147
+ font-family: 'Courier New', monospace;
148
+ font-size: 13px;
149
+ color: #666;
150
+ white-space: pre-wrap;
151
+ word-wrap: break-word;
152
+ max-height: 400px;
153
+ overflow-y: auto;
154
+ padding: 16px;
155
+ background: #f9f9f9;
156
+ border-radius: 6px;
157
+ border: 1px solid #eee;
158
+ }
159
+
160
+ .warning {
161
+ padding: 12px 16px;
162
+ background: #fef3c7;
163
+ border-radius: 6px;
164
+ font-size: 14px;
165
+ color: #92400e;
166
+ margin-bottom: 16px;
33
167
  }
34
168
  </style>
35
169
  </head>
36
170
 
37
171
  <body>
38
- <h1>听歌识曲 Demo (Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a>)</h1>
39
- <hr>
40
- <p><b>DISCLAIMER: </b></p>
41
- <p>This site uses the offical NetEase audio matcher APIs (reverse engineered from <a
42
- href="https://fn.music.163.com/g/chrome-extension-home-page-beta/">https://fn.music.163.com/g/chrome-extension-home-page-beta/</a>)
43
- </p>
44
- <p>And DOES NOT condone copyright infringment nor intellectual property theft.</p>
45
- <hr>
46
- <p><b>NOTE:</b></p>
47
- <p>Before start using the site, you may want to access this link first:</p>
48
- <a href="https://cors-anywhere.herokuapp.com/corsdemo">https://cors-anywhere.herokuapp.com/corsdemo</a>
49
- <p>Since Netease APIs do not have CORS headers, this is required to alleviate this restriction.</p>
50
- <hr>
51
- <p>Usage:</p>
52
- <li>Select your audio file through "Choose File" picker</li>
53
- <li>Hit the "Clip" button and wait for the results!</li>
54
-
55
- <audio id="audio" controls autoplay></audio>
56
- <canvas id="canvas"></canvas>
57
- <button id="invoke">Clip</button>
58
- <input type="file" name="picker" accept="*" id="file">
59
- <hr>
60
- <label for="use-mic">Mix in Microphone input</label>
61
- <input type="checkbox" name="use-mic" id="usemic">
62
- <hr>
63
- <pre id="logs"></pre>
172
+ <div class="container">
173
+ <h1>听歌识曲 Demo</h1>
174
+ <p class="subtitle">Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a></p>
175
+
176
+ <hr>
177
+
178
+ <div class="warning">
179
+ <strong>免责声明:</strong>本站点使用网易云音乐官方音频识别API(逆向自 <a href="https://fn.music.163.com/g/chrome-extension-home-page-beta/" target="_blank">Chrome 扩展页面</a>),不鼓励版权侵犯或知识产权盗窃。
180
+ </div>
181
+
182
+ <div class="section">
183
+ <h3>使用说明</h3>
184
+ <p>在使用本站点之前,您可能需要先访问以下链接:</p>
185
+ <p><a href="https://cors-anywhere.herokuapp.com/corsdemo" target="_blank">https://cors-anywhere.herokuapp.com/corsdemo</a></p>
186
+ <p>由于网易云音乐API没有CORS头,这是解决此限制的必要步骤。</p>
187
+ </div>
188
+
189
+ <div class="section">
190
+ <h3>使用方法</h3>
191
+ <ul style="padding-left: 20px; font-size: 14px; color: #555;">
192
+ <li>通过"选择文件"选择您的音频文件</li>
193
+ <li>点击"识别"按钮并等待结果</li>
194
+ </ul>
195
+ </div>
196
+
197
+ <hr>
198
+
199
+ <audio id="audio" controls autoplay></audio>
200
+ <canvas id="canvas"></canvas>
201
+
202
+ <div class="control-group">
203
+ <button id="invoke">识别</button>
204
+ <input type="file" name="picker" accept="*" id="file">
205
+ </div>
206
+
207
+ <div class="control-group">
208
+ <div class="checkbox-group">
209
+ <input type="checkbox" name="use-mic" id="usemic">
210
+ <label for="usemic">混合麦克风输入</label>
211
+ </div>
212
+ </div>
213
+
214
+ <hr>
215
+
216
+ <h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 12px;">日志</h3>
217
+ <pre id="logs"></pre>
218
+ </div>
64
219
  </body>
220
+
65
221
  <script src="./afp.wasm.js"></script>
66
222
  <script src="./afp.js"></script>
67
223
  <script type="module">
@@ -76,13 +232,17 @@
76
232
  let canvas = document.getElementById('canvas')
77
233
  let canvasCtx = canvas.getContext('2d')
78
234
  let logs = document.getElementById('logs')
79
- logs.write = line => logs.innerHTML += line + '\n'
235
+ logs.write = line => {
236
+ // Append log lines as text to avoid interpreting content as HTML
237
+ logs.appendChild(document.createTextNode(line));
238
+ logs.appendChild(document.createElement('br'));
239
+ }
80
240
 
81
241
  function RecorderCallback(channelL) {
82
242
  let sampleBuffer = new Float32Array(channelL.subarray(0, duration * 8000))
83
243
  GenerateFP(sampleBuffer).then(FP => {
84
- logs.write(`[index] Generated FP ${FP}`)
85
- logs.write('[index] Now querying, please wait...')
244
+ logs.write(`[index] 生成指纹 ${FP}`)
245
+ logs.write('[index] 正在查询,请稍候...')
86
246
  fetch(
87
247
  '/audio/match?' +
88
248
  new URLSearchParams({
@@ -91,9 +251,9 @@
91
251
  method: 'POST'
92
252
  }).then(resp => resp.json()).then(resp => {
93
253
  if (!resp.data.result) {
94
- return logs.write('[index] Query failed with no results.')
254
+ return logs.write('[index] 查询失败,无结果')
95
255
  }
96
- logs.write(`[index] Query complete. Results=${resp.data.result.length}`)
256
+ logs.write(`[index] 查询完成。结果数量=${resp.data.result.length}`)
97
257
  for (var song of resp.data.result) {
98
258
  logs.write(
99
259
  `[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 @@
104
264
  }
105
265
 
106
266
  function InitAudioCtx() {
107
- // AFP.wasm can't do it with anything other than 8KHz
108
267
  audioCtx = new AudioContext({ 'sampleRate': 8000 })
109
268
  if (audioCtx.state == 'suspended')
110
269
  return false
111
270
  let audioNode = audioCtx.createMediaElementSource(audio)
112
271
  audioCtx.audioWorklet.addModule('rec.js').then(() => {
113
272
  recorderNode = new AudioWorkletNode(audioCtx, 'timed-recorder')
114
- audioNode.connect(recorderNode) // recorderNode doesn't output anything
273
+ audioNode.connect(recorderNode)
115
274
  audioNode.connect(audioCtx.destination)
116
275
  recorderNode.port.onmessage = event => {
117
276
  switch (event.data.message) {
118
277
  case 'finished':
119
278
  RecorderCallback(event.data.recording)
120
- clip.innerHTML = 'Clip'
279
+ clip.innerHTML = '识别'
121
280
  clip.disabled = false
122
281
  canvas.classList.remove('canvas-active')
123
282
  break
@@ -130,7 +289,6 @@
130
289
  logs.write(event.data.message)
131
290
  }
132
291
  }
133
- // Attempt to get user's microphone and connect it to the AudioContext.
134
292
  navigator.mediaDevices.getUserMedia({
135
293
  audio: {
136
294
  echoCancellation: false,
@@ -142,7 +300,7 @@
142
300
  micSourceNode = audioCtx.createMediaStreamSource(micStream);
143
301
  micSourceNode.connect(recorderNode)
144
302
  usemic.checked = true
145
- logs.write('[rec.js] Microphone attached.')
303
+ logs.write('[rec.js] 麦克风已连接')
146
304
  });
147
305
  });
148
306
  return true
@@ -161,10 +319,20 @@
161
319
  else
162
320
  micSourceNode.connect(recorderNode)
163
321
  })
322
+ function escapeHtml(str) {
323
+ return String(str)
324
+ .replace(/&/g, '&amp;')
325
+ .replace(/</g, '&lt;')
326
+ .replace(/>/g, '&gt;')
327
+ .replace(/"/g, '&quot;')
328
+ .replace(/'/g, '&#39;')
329
+ .replace(/\//g, '&#x2F;');
330
+ }
164
331
  file.addEventListener('change', event => {
165
332
  file.files[0].arrayBuffer().then(
166
333
  async buffer => {
167
- logs.write(`[index] File ${file.files[0].name} loaded.`)
334
+ const safeName = escapeHtml(file.files[0].name)
335
+ logs.write(`[index] 文件 ${safeName} 已加载`)
168
336
  audio.src = window.URL.createObjectURL(new Blob([buffer]))
169
337
  clip.disabled = false
170
338
  })
@@ -188,12 +356,13 @@
188
356
  UpdateCanvas()
189
357
  let requestCtx = setInterval(() => {
190
358
  try {
191
- if (InitAudioCtx()) { // Put this here so we don't have to deal with the 'user did not interact' thing
359
+ if (InitAudioCtx()) {
192
360
  clearInterval(requestCtx)
193
- logs.write('[rec.js] Audio Context started.')
361
+ logs.write('[rec.js] 音频上下文已启动')
194
362
  }
195
363
  } catch {
196
364
  // Fail silently
197
365
  }
198
366
  }, 100)
199
367
  </script>
368
+ </html>