lzwqx commited on
Commit
9066016
·
verified ·
1 Parent(s): c63ecce

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +253 -48
app.js CHANGED
@@ -4,20 +4,19 @@ const fs = require('fs');
4
  const rangeParser = require('range-parser');
5
  const bytes = require('bytes');
6
  const NodeCache = require('node-cache');
7
- const axios = require('axios');
8
- const os = require('os');
9
  const app = express();
10
- const PORT = process.env.PORT || 7860;
11
 
12
- require('dotenv').config();
13
- const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin';
14
- const musicDir = path.join(process.env.MUSIC_DIR || path.join(os.homedir(), 'music'));
15
 
 
 
 
 
16
  if (!fs.existsSync(musicDir)) {
17
  fs.mkdirSync(musicDir, { recursive: true });
18
  console.log(`Created music directory: ${musicDir}`);
19
- } else {
20
- console.log(`Using existing music directory: ${musicDir}`);
21
  }
22
 
23
  function getContentType(ext) {
@@ -30,13 +29,26 @@ function getContentType(ext) {
30
  return contentTypes[ext] || 'application/octet-stream';
31
  }
32
 
33
- const cache = new NodeCache({ stdTTL: 7200, checkperiod: 120, maxKeys: 500 });
34
- const stats = { totalBytes: 0, requests: 0 };
 
 
 
 
 
 
 
 
 
 
35
 
 
36
  app.set('json spaces', 2);
 
 
37
  app.use('/static', express.static(musicDir));
38
- app.use(express.static(path.join(__dirname, 'public')));
39
 
 
40
  app.use((req, res, next) => {
41
  res.header('Access-Control-Allow-Origin', '*');
42
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
@@ -44,9 +56,15 @@ app.use((req, res, next) => {
44
  next();
45
  });
46
 
 
 
 
 
47
  app.get('/music/:filename', async (req, res) => {
48
  const filename = req.params.filename;
49
- if (!filename.match(/^[a-zA-Z0-9一-龥][a-zA-Z0-9一-龥\s\-_.]+\.(mp3|wav|flac|m4a)$/)) {
 
 
50
  return res.status(400).send('Invalid filename');
51
  }
52
 
@@ -56,6 +74,8 @@ app.get('/music/:filename', async (req, res) => {
56
  }
57
 
58
  const filepath = path.join(musicDir, filename);
 
 
59
  let fileInfo = cache.get(filepath);
60
  if (!fileInfo) {
61
  try {
@@ -73,6 +93,7 @@ app.get('/music/:filename', async (req, res) => {
73
 
74
  const range = req.headers.range;
75
 
 
76
  res.set({
77
  'Cache-Control': 'public, max-age=3600',
78
  'Last-Modified': fileInfo.mtime,
@@ -82,78 +103,262 @@ app.get('/music/:filename', async (req, res) => {
82
  'X-Content-Type-Options': 'nosniff'
83
  });
84
 
 
85
  if (range) {
86
  const ranges = rangeParser(fileInfo.size, range);
 
87
  if (ranges === -1 || ranges === -2) {
88
  return res.status(416).send('Range not satisfiable');
89
  }
 
90
  const { start, end } = ranges[0];
91
  const chunk = end - start + 1;
 
92
  res.status(206);
93
  res.set({
94
  'Content-Range': `bytes ${start}-${end}/${fileInfo.size}`,
95
  'Content-Length': chunk
96
  });
97
- const stream = fs.createReadStream(filepath, { start, end, highWaterMark: 64 * 1024 });
 
 
 
 
 
 
98
  stats.totalBytes += chunk;
99
  stats.requests += 1;
100
- stream.on('error', err => {
101
- console.error(`Stream error for ${filename}:`, err);
102
- if (!res.headersSent) res.status(500).send('Internal server error');
 
 
 
103
  });
 
104
  stream.pipe(res);
105
  } else {
106
- res.set({ 'Content-Length': fileInfo.size });
107
- const stream = fs.createReadStream(filepath, { highWaterMark: 64 * 1024 });
 
 
 
 
 
 
108
  stats.totalBytes += fileInfo.size;
109
  stats.requests += 1;
110
- stream.on('error', err => {
111
- console.error(`Stream error for ${filename}:`, err);
112
- if (!res.headersSent) res.status(500).send('Internal server error');
 
 
 
113
  });
 
114
  stream.pipe(res);
115
  }
116
  });
117
 
 
 
 
 
 
 
 
 
 
118
  app.get('/api/download', async (req, res) => {
119
  const { url, name } = req.query;
120
- if (!url) return res.status(400).json({ error: 'Please provide a music url' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  try {
122
- const urlFileName = decodeURIComponent(path.basename(new URL(url).pathname));
123
- const urlExt = path.extname(urlFileName).toLowerCase();
124
- const validExts = ['.mp3', '.wav', '.flac', '.m4a'];
125
- if (!validExts.includes(urlExt)) {
126
- return res.status(400).json({ error: 'Unsupported file format' });
127
- }
128
- const fullName = name ? `${name}${urlExt}` : urlFileName;
129
- if (!fullName.match(/^[a-zA-Z0-9一-龥][a-zA-Z0-9一-龥\s\-_.]+\.(mp3|wav|flac|m4a)$/)) {
130
- return res.status(400).json({ error: 'Invalid filename' });
131
- }
132
- const savePath = path.join(musicDir, fullName);
133
- if (fs.existsSync(savePath)) {
134
- const fileUrl = `${req.protocol}://${req.get('host')}/music/${encodeURIComponent(fullName)}`;
135
- return res.status(200).json({ warning: 'File already exists', url: fileUrl });
136
- }
137
- const futureUrl = `${req.protocol}://${req.get('host')}/music/${encodeURIComponent(fullName)}`;
138
- res.status(202).json({ success: true, message: `Downloading "${fullName}" from "${url}"`, filename: fullName, futureUrl });
139
- console.log(`📥 Start downloading: ${fullName} from ${url}`);
140
- const response = await axios({ method: 'GET', url, timeout: 300000, responseType: 'stream' });
141
  const writer = fs.createWriteStream(savePath);
 
142
  response.data.pipe(writer);
143
- writer.on('error', err => {
144
- console.error(`❌ Write error for ${fullName}:`, err.message);
 
145
  fs.unlink(savePath, () => {});
146
  });
 
147
  writer.on('finish', () => {
148
- console.log(`Download finished: ${fullName}`);
149
  });
150
- } catch (err) {
151
- console.error(`Download failed for url=${url}`, err.message);
 
152
  }
153
  });
154
 
155
- // ... 其余保持不变 ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
 
157
  app.listen(PORT, () => {
158
- console.log(`🎵 music service is running on port ${PORT}`);
159
  });
 
4
  const rangeParser = require('range-parser');
5
  const bytes = require('bytes');
6
  const NodeCache = require('node-cache');
7
+ const axios = require('axios');
 
8
  const app = express();
9
+ const PORT = process.env.PORT || 3000;
10
 
11
+ require('dotenv').config(); // 加载环境变量
 
 
12
 
13
+ const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin'; // 管理密码
14
+ const musicDir = path.join(__dirname, process.env.MUSIC_DIR || 'music');
15
+
16
+ // 确保音乐目录存在,不存在自动创建
17
  if (!fs.existsSync(musicDir)) {
18
  fs.mkdirSync(musicDir, { recursive: true });
19
  console.log(`Created music directory: ${musicDir}`);
 
 
20
  }
21
 
22
  function getContentType(ext) {
 
29
  return contentTypes[ext] || 'application/octet-stream';
30
  }
31
 
32
+ // 创建缓存实例,TTL 设置为1小时
33
+ const cache = new NodeCache({
34
+ stdTTL: 7200,
35
+ checkperiod: 120,
36
+ maxKeys: 500 // 最多缓存500个文件的信息
37
+ });
38
+
39
+ // 流量统计
40
+ const stats = {
41
+ totalBytes: 0,
42
+ requests: 0
43
+ };
44
 
45
+ // JSON 格式化
46
  app.set('json spaces', 2);
47
+
48
+ // 静态文件服务
49
  app.use('/static', express.static(musicDir));
 
50
 
51
+ // CORS 中间件
52
  app.use((req, res, next) => {
53
  res.header('Access-Control-Allow-Origin', '*');
54
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
 
56
  next();
57
  });
58
 
59
+ // 前端静态文件服务
60
+ app.use(express.static(path.join(__dirname, 'public')));
61
+
62
+ // 直链生成
63
  app.get('/music/:filename', async (req, res) => {
64
  const filename = req.params.filename;
65
+
66
+ // 检查文件名是否合法
67
+ if (!filename.match(/^[a-zA-Z0-9\u4e00-\u9fa5][a-zA-Z0-9\u4e00-\u9fa5\s\-_.]+\.(mp3|wav|flac|m4a)$/)) {
68
  return res.status(400).send('Invalid filename');
69
  }
70
 
 
74
  }
75
 
76
  const filepath = path.join(musicDir, filename);
77
+
78
+ // 从缓存获取文件信息
79
  let fileInfo = cache.get(filepath);
80
  if (!fileInfo) {
81
  try {
 
93
 
94
  const range = req.headers.range;
95
 
96
+ // 通用响应头
97
  res.set({
98
  'Cache-Control': 'public, max-age=3600',
99
  'Last-Modified': fileInfo.mtime,
 
103
  'X-Content-Type-Options': 'nosniff'
104
  });
105
 
106
+ // 处理范围请求
107
  if (range) {
108
  const ranges = rangeParser(fileInfo.size, range);
109
+
110
  if (ranges === -1 || ranges === -2) {
111
  return res.status(416).send('Range not satisfiable');
112
  }
113
+
114
  const { start, end } = ranges[0];
115
  const chunk = end - start + 1;
116
+
117
  res.status(206);
118
  res.set({
119
  'Content-Range': `bytes ${start}-${end}/${fileInfo.size}`,
120
  'Content-Length': chunk
121
  });
122
+
123
+ const stream = fs.createReadStream(filepath, {
124
+ start,
125
+ end,
126
+ highWaterMark: 64 * 1024 // 64KB 缓冲区
127
+ });
128
+
129
  stats.totalBytes += chunk;
130
  stats.requests += 1;
131
+
132
+ stream.on('error', (error) => {
133
+ console.error(`Stream error for ${filename}:`, error);
134
+ if (!res.headersSent) {
135
+ res.status(500).send('Internal server error');
136
+ }
137
  });
138
+
139
  stream.pipe(res);
140
  } else {
141
+ res.set({
142
+ 'Content-Length': fileInfo.size
143
+ });
144
+
145
+ const stream = fs.createReadStream(filepath, {
146
+ highWaterMark: 64 * 1024 // 64KB 缓冲区
147
+ });
148
+
149
  stats.totalBytes += fileInfo.size;
150
  stats.requests += 1;
151
+
152
+ stream.on('error', (error) => {
153
+ console.error(`Stream error for ${filename}:`, error);
154
+ if (!res.headersSent) {
155
+ res.status(500).send('Internal server error');
156
+ }
157
  });
158
+
159
  stream.pipe(res);
160
  }
161
  });
162
 
163
+ // 统计接口
164
+ app.get('/stats', (req, res) => {
165
+ res.json({
166
+ totalTransferred: bytes(stats.totalBytes),
167
+ totalRequests: stats.requests
168
+ });
169
+ });
170
+
171
+ // 下载音乐API
172
  app.get('/api/download', async (req, res) => {
173
  const { url, name } = req.query;
174
+
175
+ if (!url) {
176
+ return res.status(400).json({ error: 'Please provide a music url' });
177
+ }
178
+
179
+ // 从 URL 中获取文件名和扩展名
180
+ const urlFileName = decodeURIComponent(path.basename(url));
181
+ const urlExt = path.extname(urlFileName).toLowerCase();
182
+
183
+ if (!['.mp3', '.wav', '.flac', '.m4a'].includes(urlExt)) {
184
+ return res.status(400).json({ error: 'Unsupported file format' });
185
+ }
186
+
187
+ // 使用提供的文件名或 URL 中的文件名
188
+ const fullName = name ? (name + urlExt) : urlFileName;
189
+
190
+ // 验证文件名格式
191
+ if (!fullName.match(/^[a-zA-Z0-9\u4e00-\u9fa5][a-zA-Z0-9\u4e00-\u9fa5\s\-_.]+\.(mp3|wav|flac|m4a)$/)) {
192
+ return res.status(400).json({ error: 'filename is wrong' });
193
+ }
194
+
195
+ const savePath = path.join(musicDir, fullName);
196
+
197
+ // 检查文件是否已存在
198
+ if (fs.existsSync(savePath)) {
199
+ const protocol = req.headers['x-forwarded-proto'] || req.protocol;
200
+ const host = req.get('host');
201
+ const fileUrl = `${protocol}://${host}/music/${encodeURIComponent(fullName)}`;
202
+
203
+ return res.status(200).json({
204
+ warning: 'The song already exists',
205
+ url: fileUrl
206
+ });
207
+ }
208
+
209
+ // api返回响应
210
+ const protocol = req.headers['x-forwarded-proto'] || req.protocol;
211
+ const host = req.get('host');
212
+
213
+ res.json({
214
+ success: true,
215
+ message: 'The song added to download list successfully',
216
+ filename: fullName,
217
+ futureUrl: `${protocol}://${host}/music/${encodeURIComponent(fullName)}`,
218
+ });
219
+
220
+ // 将音乐加入后台异步下载
221
  try {
222
+ const response = await axios({
223
+ method: 'GET',
224
+ url: url,
225
+ timeout: 300000,
226
+ responseType: 'stream'
227
+ });
228
+
 
 
 
 
 
 
 
 
 
 
 
 
229
  const writer = fs.createWriteStream(savePath);
230
+
231
  response.data.pipe(writer);
232
+
233
+ writer.on('error', (err) => {
234
+ console.error(`Download error for ${fullName}:`, err.message);
235
  fs.unlink(savePath, () => {});
236
  });
237
+
238
  writer.on('finish', () => {
239
+ console.log(`Download finished ${fullName}`);
240
  });
241
+ } catch (error) {
242
+ console.error(`Download failed for ${fullName}:`, error.message);
243
+ fs.unlink(savePath, () => {});
244
  }
245
  });
246
 
247
+ // 获取音乐文件大小
248
+ function formatFileSize(bytes) {
249
+ const units = ['B', 'KB', 'MB', 'GB'];
250
+ let size = bytes;
251
+ let unitIndex = 0;
252
+
253
+ while (size >= 1024 && unitIndex < units.length - 1) {
254
+ size /= 1024;
255
+ unitIndex++;
256
+ }
257
+
258
+ return `${size.toFixed(2)}${units[unitIndex]}`;
259
+ }
260
+
261
+ // 获取音乐列表 API
262
+ app.get('/api/music/list', async (req, res) => {
263
+ try {
264
+ const files = await fs.promises.readdir(musicDir);
265
+ const musicFiles = files.filter(file =>
266
+ ['.mp3', '.wav', '.flac', '.m4a'].includes(path.extname(file).toLowerCase())
267
+ );
268
+
269
+ // 获取当前请求的完整URL
270
+ const currentUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
271
+ const urlObj = new URL(currentUrl);
272
+ // 使用 x-forwarded-proto 头来判断实际协议
273
+ const protocol = req.headers['x-forwarded-proto'] || urlObj.protocol;
274
+ const host = urlObj.host;
275
+
276
+ const musicList = await Promise.all(musicFiles.map(async file => {
277
+ const filePath = path.join(musicDir, file);
278
+ const stat = await fs.promises.stat(filePath);
279
+ return {
280
+ filename: file,
281
+ url: `${protocol}://${host}/music/${encodeURIComponent(file)}`,
282
+ size: formatFileSize(stat.size),
283
+ extension: path.extname(file).slice(1).toUpperCase(),
284
+ lastModified: stat.mtime.toLocaleString()
285
+ };
286
+ }));
287
+
288
+ res.json({
289
+ total: musicList.length,
290
+ data: musicList
291
+ });
292
+ } catch (error) {
293
+ res.status(500).json({
294
+ error: 'Get music list failed',
295
+ details: error.message
296
+ });
297
+ }
298
+ });
299
+
300
+ // 删除音乐API - 使用POST请求
301
+ app.post('/api/delete/music', async (req, res) => {
302
+ const { names, password, all } = req.query;
303
+
304
+ // 验证管理密码
305
+ if (password !== ADMIN_PASSWORD) {
306
+ return res.status(401).json({ error: 'Unauthorized: Invalid password' });
307
+ }
308
+
309
+ try {
310
+ let filesToDelete = [];
311
+
312
+ // 情况1: 删除所有音乐文件
313
+ if (all === 'true') {
314
+ const files = await fs.promises.readdir(musicDir);
315
+ filesToDelete = files.filter(file =>
316
+ ['.mp3', '.wav', '.flac', '.m4a'].includes(path.extname(file).toLowerCase())
317
+ );
318
+ }
319
+ // 情况2: 批量删除指定名称的音乐文件
320
+ else if (names) {
321
+ const nameList = typeof names === 'string' ? names.split(',') : names;
322
+ const files = await fs.promises.readdir(musicDir);
323
+
324
+ filesToDelete = files.filter(file => {
325
+ const filenameWithoutExt = path.basename(file, path.extname(file));
326
+ const songNamePart = filenameWithoutExt.split('-')[0].trim().toLowerCase();
327
+ return nameList.some(name =>
328
+ songNamePart === name.trim().toLowerCase() &&
329
+ ['.mp3', '.wav', '.flac', '.m4a'].includes(path.extname(file).toLowerCase())
330
+ );
331
+ });
332
+ }
333
+ else {
334
+ return res.status(400).json({ error: 'Please provide names parameter or set all=true' });
335
+ }
336
+
337
+ if (filesToDelete.length === 0) {
338
+ return res.status(404).json({ error: 'No matching songs found' });
339
+ }
340
+
341
+ // 删除所有匹配的文件
342
+ await Promise.all(filesToDelete.map(async file => {
343
+ const filePath = path.join(musicDir, file);
344
+ await fs.promises.unlink(filePath);
345
+ cache.del(filePath);
346
+ }));
347
+
348
+ res.json({
349
+ success: true,
350
+ message: `Deleted ${filesToDelete.length} song(s)`,
351
+ deletedFiles: filesToDelete
352
+ });
353
+ } catch (error) {
354
+ res.status(500).json({
355
+ error: 'Failed to delete song(s)',
356
+ details: error.message
357
+ });
358
+ }
359
+ });
360
 
361
+ // 启动服务器
362
  app.listen(PORT, () => {
363
+ console.log(`music service is running on port ${PORT}`);
364
  });