@lzwme/m3u8-dl 1.6.0 → 1.7.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 +8 -8
- package/README.zh-CN.md +8 -8
- package/cjs/i18n/locales/en.d.ts +9 -0
- package/cjs/i18n/locales/en.js +9 -0
- package/cjs/i18n/locales/zh-CN.d.ts +9 -0
- package/cjs/i18n/locales/zh-CN.js +9 -0
- package/cjs/lib/local-play.js +1 -1
- package/cjs/lib/utils.d.ts +2 -0
- package/cjs/lib/utils.js +14 -0
- package/cjs/server/download-server.d.ts +11 -4
- package/cjs/server/download-server.js +167 -71
- package/client/assets/main-BSWj1VKy.js +29 -0
- package/client/assets/main-T6xR17Gh.css +1 -0
- package/client/index.html +2 -2
- package/client/m3u8-capture.user.js +40 -20
- package/client/play.html +36 -19
- package/package.json +8 -4
- package/client/assets/main-ChJ1yjNN.css +0 -1
- package/client/assets/main-DZTEqg-V.js +0 -29
|
@@ -60,7 +60,7 @@ class DLServer {
|
|
|
60
60
|
};
|
|
61
61
|
serverInfo = {
|
|
62
62
|
version: '',
|
|
63
|
-
ariang:
|
|
63
|
+
ariang: false,
|
|
64
64
|
};
|
|
65
65
|
cfg = {
|
|
66
66
|
/** 支持 web 设置修改的参数 */
|
|
@@ -91,28 +91,29 @@ class DLServer {
|
|
|
91
91
|
opts.cacheDir = (0, node_path_1.resolve)(opts.cacheDir);
|
|
92
92
|
if (!opts.configPath)
|
|
93
93
|
opts.configPath = (0, node_path_1.resolve)(opts.cacheDir, 'config.json');
|
|
94
|
-
const pkgFile = (0, node_path_1.resolve)(rootDir, 'package.json');
|
|
95
|
-
if ((0, node_fs_1.existsSync)(pkgFile)) {
|
|
96
|
-
const pkg = JSON.parse((0, node_fs_1.readFileSync)(pkgFile, 'utf8'));
|
|
97
|
-
this.serverInfo.version = pkg.version;
|
|
98
|
-
}
|
|
99
94
|
if (opts.token)
|
|
100
95
|
opts.token = (0, fe_utils_1.md5)(opts.token.trim()).slice(0, 8);
|
|
101
96
|
this.init();
|
|
102
97
|
}
|
|
103
98
|
async init() {
|
|
104
|
-
|
|
99
|
+
const pkgFile = (0, node_path_1.resolve)(rootDir, 'package.json');
|
|
100
|
+
if (await (0, utils_js_1.checkFileExists)(pkgFile)) {
|
|
101
|
+
const pkg = JSON.parse(await node_fs_1.promises.readFile(pkgFile, 'utf8'));
|
|
102
|
+
this.serverInfo.version = pkg.version;
|
|
103
|
+
}
|
|
104
|
+
this.serverInfo.ariang = await (0, utils_js_1.checkFileExists)((0, node_path_1.resolve)(rootDir, 'client/ariang/index.html'));
|
|
105
|
+
await this.loadConfig();
|
|
105
106
|
if (this.cfg.dlOptions.debug)
|
|
106
107
|
utils_js_1.logger.updateOptions({ levelType: 'debug' });
|
|
107
|
-
this.loadCache();
|
|
108
|
+
await this.loadCache();
|
|
108
109
|
await this.createApp();
|
|
109
110
|
this.initRouters();
|
|
110
111
|
utils_js_1.logger.debug('Server initialized', 'cacheSize:', this.dlCache.size, this.options, this.cfg.dlOptions);
|
|
111
112
|
}
|
|
112
|
-
loadCache() {
|
|
113
|
+
async loadCache() {
|
|
113
114
|
const cacheFile = (0, node_path_1.resolve)(this.options.cacheDir, 'cache.json');
|
|
114
|
-
if ((0,
|
|
115
|
-
JSON.parse(
|
|
115
|
+
if (await (0, utils_js_1.checkFileExists)(cacheFile)) {
|
|
116
|
+
JSON.parse(await node_fs_1.promises.readFile(cacheFile, 'utf8')).forEach(([url, item]) => {
|
|
116
117
|
if (item.status === 'resume')
|
|
117
118
|
item.status = 'pause';
|
|
118
119
|
this.dlCache.set(url, item);
|
|
@@ -122,20 +123,39 @@ class DLServer {
|
|
|
122
123
|
}
|
|
123
124
|
checkDLFileLaest = 0;
|
|
124
125
|
checkDLFileTimer = null;
|
|
125
|
-
checkDLFileIsExists() {
|
|
126
|
+
async checkDLFileIsExists() {
|
|
126
127
|
const now = Date.now();
|
|
127
128
|
const interval = 1000 * 60;
|
|
128
129
|
clearTimeout(this.checkDLFileTimer);
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
const delay = this.checkDLFileLaest + interval - now;
|
|
131
|
+
if (delay > 0) {
|
|
132
|
+
this.checkDLFileTimer = setTimeout(() => {
|
|
133
|
+
this.checkDLFileIsExists();
|
|
134
|
+
}, delay + 100);
|
|
131
135
|
return;
|
|
132
136
|
}
|
|
133
|
-
this.dlCache.
|
|
134
|
-
|
|
137
|
+
const tasks = [...this.dlCache.values()].map(item => () => this.checkItemStatus(item));
|
|
138
|
+
await (0, fe_utils_1.concurrency)(tasks, 3);
|
|
139
|
+
this.checkDLFileLaest = now;
|
|
140
|
+
}
|
|
141
|
+
async checkItemStatus(item) {
|
|
142
|
+
if (item.status === 'done') {
|
|
143
|
+
if (item.current) {
|
|
144
|
+
if (!item.cacheDir && item.current.tsOut)
|
|
145
|
+
item.cacheDir = (0, node_path_1.dirname)(item.current.tsOut);
|
|
146
|
+
delete item.current;
|
|
147
|
+
}
|
|
148
|
+
if (!(await (0, utils_js_1.checkFileExists)(item.localVideo)) && !(0, node_fs_1.existsSync)(item.localVideo)) {
|
|
135
149
|
item.status = 'error';
|
|
136
150
|
item.errmsg = '已删除';
|
|
137
151
|
}
|
|
138
|
-
}
|
|
152
|
+
}
|
|
153
|
+
else if (item.status === 'error' && item.progress === 100) {
|
|
154
|
+
if (await (0, utils_js_1.checkFileExists)(item.localVideo)) {
|
|
155
|
+
item.status = 'done';
|
|
156
|
+
delete item.errmsg;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
139
159
|
}
|
|
140
160
|
dlCacheClone() {
|
|
141
161
|
const info = [];
|
|
@@ -154,34 +174,32 @@ class DLServer {
|
|
|
154
174
|
clearTimeout(this.cacheSaveTimer);
|
|
155
175
|
this.cacheSaveTimer = setTimeout(() => {
|
|
156
176
|
const cacheFile = (0, node_path_1.resolve)(this.options.cacheDir, 'cache.json');
|
|
157
|
-
const info = this.dlCacheClone();
|
|
158
177
|
(0, fe_utils_1.mkdirp)((0, node_path_1.dirname)(cacheFile));
|
|
159
|
-
|
|
178
|
+
node_fs_1.promises.writeFile(cacheFile, JSON.stringify(this.dlCacheClone()));
|
|
160
179
|
}, 1000);
|
|
161
180
|
}
|
|
162
|
-
|
|
181
|
+
async loadConfig(configPath) {
|
|
163
182
|
try {
|
|
164
183
|
if (!configPath)
|
|
165
184
|
configPath = this.options.configPath;
|
|
166
|
-
if ((0,
|
|
167
|
-
(0, fe_utils_1.assign)(this.cfg, JSON.parse(
|
|
185
|
+
if (await (0, utils_js_1.checkFileExists)(configPath))
|
|
186
|
+
(0, fe_utils_1.assign)(this.cfg, JSON.parse(await node_fs_1.promises.readFile(configPath, 'utf8')));
|
|
168
187
|
}
|
|
169
188
|
catch (error) {
|
|
170
|
-
utils_js_1.logger.error('
|
|
189
|
+
utils_js_1.logger.error('Load config failed:', error);
|
|
171
190
|
}
|
|
172
|
-
return this.cfg.dlOptions;
|
|
173
191
|
}
|
|
174
|
-
saveConfig(config, configPath) {
|
|
192
|
+
async saveConfig(config, configPath) {
|
|
175
193
|
if (!configPath)
|
|
176
194
|
configPath = this.options.configPath;
|
|
177
195
|
// 验证 ffmpegPath 是否存在
|
|
178
196
|
if (config.ffmpegPath?.trim()) {
|
|
179
197
|
const ffmpegPath = config.ffmpegPath.trim();
|
|
180
|
-
if (!(0,
|
|
198
|
+
if (!(await (0, utils_js_1.checkFileExists)(ffmpegPath))) {
|
|
181
199
|
throw new Error(`ffmpeg 路径不存在: ${ffmpegPath}`);
|
|
182
200
|
}
|
|
183
201
|
// 检查是否为文件(不是目录)
|
|
184
|
-
const stats =
|
|
202
|
+
const stats = await node_fs_1.promises.stat(ffmpegPath);
|
|
185
203
|
if (!stats.isFile()) {
|
|
186
204
|
throw new Error(`ffmpeg 路径不是文件: ${ffmpegPath}`);
|
|
187
205
|
}
|
|
@@ -195,7 +213,7 @@ class DLServer {
|
|
|
195
213
|
this.cfg.dlOptions[key] = value;
|
|
196
214
|
}
|
|
197
215
|
(0, fe_utils_1.mkdirp)((0, node_path_1.dirname)(configPath));
|
|
198
|
-
|
|
216
|
+
return node_fs_1.promises.writeFile(configPath, JSON.stringify(this.cfg, null, 2));
|
|
199
217
|
}
|
|
200
218
|
async createApp() {
|
|
201
219
|
const { default: express } = await Promise.resolve().then(() => __importStar(require('express')));
|
|
@@ -203,17 +221,17 @@ class DLServer {
|
|
|
203
221
|
const app = express();
|
|
204
222
|
const server = app.listen(this.options.port, () => utils_js_1.logger.info(`Server running on port ${(0, console_log_colors_1.green)(this.options.port)}`));
|
|
205
223
|
const wss = new WebSocketServer({ server });
|
|
224
|
+
const hasLocalCdnDir = await (0, utils_js_1.checkFileExists)((0, node_path_1.resolve)(rootDir, 'client/local/cdn'));
|
|
206
225
|
this.app = app;
|
|
207
226
|
this.wss = wss;
|
|
208
|
-
app.use((req, res, next) => {
|
|
227
|
+
app.use(async (req, res, next) => {
|
|
209
228
|
// 处理 SPA 路由:根路径和 /page/* 路径都返回 index.html
|
|
210
229
|
const isIndexPage = ['/', '/index.html'].includes(req.path) || req.path.startsWith('/page/');
|
|
211
230
|
const isPlayPage = req.path.startsWith('/play.html');
|
|
212
231
|
const isApi = req.path.startsWith('/api/');
|
|
213
232
|
if (!isApi && (isIndexPage || isPlayPage)) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(rootDir, 'client/local/cdn'))) {
|
|
233
|
+
let htmlContent = await node_fs_1.promises.readFile((0, node_path_1.resolve)(rootDir, `client/${isPlayPage ? 'play' : 'index'}.html`), 'utf-8');
|
|
234
|
+
if (hasLocalCdnDir) {
|
|
217
235
|
// 提取所有 zstatic.net 的 js 和 css 资源地址,若子路径存在于 local/cdn 目录下则替换为本地路径
|
|
218
236
|
const zstaticRegex = /https:\/\/s4\.zstatic\.net\/ajax\/libs\/[^\s"'`<>]+\.(js|css)/g;
|
|
219
237
|
const zstaticMatches = htmlContent.match(zstaticRegex);
|
|
@@ -221,7 +239,7 @@ class DLServer {
|
|
|
221
239
|
for (const match of zstaticMatches) {
|
|
222
240
|
const relativePath = match.split('libs/')[1];
|
|
223
241
|
const localPath = (0, node_path_1.resolve)(rootDir, `client/local/cdn/${relativePath}`);
|
|
224
|
-
if ((0,
|
|
242
|
+
if (await (0, utils_js_1.checkFileExists)(localPath)) {
|
|
225
243
|
htmlContent = htmlContent.replaceAll(match, `/local/cdn/${relativePath}`);
|
|
226
244
|
}
|
|
227
245
|
}
|
|
@@ -289,14 +307,23 @@ class DLServer {
|
|
|
289
307
|
options.filename = item[1];
|
|
290
308
|
}
|
|
291
309
|
const { options: dlOptions } = (0, format_options_js_1.formatOptions)(url, { ...this.cfg.dlOptions, ...options, cacheDir: this.options.cacheDir });
|
|
310
|
+
if (!dlOptions.saveDir)
|
|
311
|
+
dlOptions.saveDir = this.cfg.dlOptions.saveDir;
|
|
292
312
|
const cacheItem = this.dlCache.get(url) || { options, dlOptions, status: 'pending', url };
|
|
293
313
|
utils_js_1.logger.debug('startDownload', url, dlOptions, cacheItem.status);
|
|
294
314
|
if (cacheItem.status === 'resume')
|
|
295
315
|
return;
|
|
296
|
-
if (cacheItem.localVideo
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
316
|
+
if (cacheItem.localVideo) {
|
|
317
|
+
if (await (0, utils_js_1.checkFileExists)(cacheItem.localVideo)) {
|
|
318
|
+
if (cacheItem.status === 'done')
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
delete cacheItem.localVideo;
|
|
323
|
+
if (cacheItem.endTime)
|
|
324
|
+
delete cacheItem.endTime;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
300
327
|
cacheItem.status = this.downloading >= this.cfg.webOptions.maxDownloads ? 'pending' : 'resume';
|
|
301
328
|
// pending 优先级靠后
|
|
302
329
|
if (cacheItem.status === 'pending' && this.dlCache.has(url))
|
|
@@ -317,18 +344,20 @@ class DLServer {
|
|
|
317
344
|
if (!item)
|
|
318
345
|
return false; // 已删除
|
|
319
346
|
const status = item.status || 'resume';
|
|
320
|
-
|
|
347
|
+
if (!item.cacheDir && current?.tsOut)
|
|
348
|
+
item.cacheDir = (0, node_path_1.dirname)(current.tsOut);
|
|
349
|
+
Object.assign(item, { ...stats, dlOptions, status, workPoll, url });
|
|
321
350
|
this.dlCache.set(url, item);
|
|
322
351
|
this.saveCache();
|
|
323
352
|
this.wsSend('progress', url);
|
|
324
353
|
return status !== 'pause';
|
|
325
354
|
},
|
|
326
355
|
};
|
|
327
|
-
const afterDownload = (r, url) => {
|
|
356
|
+
const afterDownload = async (r, url) => {
|
|
328
357
|
const item = this.dlCache.get(url) || cacheItem;
|
|
329
|
-
if (r.filepath && (0,
|
|
358
|
+
if (r.filepath && (await (0, utils_js_1.checkFileExists)(r.filepath))) {
|
|
330
359
|
item.localVideo = r.filepath;
|
|
331
|
-
item.downloadedSize = (
|
|
360
|
+
item.downloadedSize = (await node_fs_1.promises.stat(r.filepath)).size;
|
|
332
361
|
}
|
|
333
362
|
else if (!r.errmsg && opts.convert !== false)
|
|
334
363
|
r.errmsg = '下载失败';
|
|
@@ -372,6 +401,11 @@ class DLServer {
|
|
|
372
401
|
if (queryLang && i18n_js_1.LANG_CODES.has(queryLang)) {
|
|
373
402
|
return queryLang;
|
|
374
403
|
}
|
|
404
|
+
// Try to get lang from body
|
|
405
|
+
const bodyLang = req.body?.lang;
|
|
406
|
+
if (bodyLang && i18n_js_1.LANG_CODES.has(bodyLang)) {
|
|
407
|
+
return bodyLang;
|
|
408
|
+
}
|
|
375
409
|
// Try to get lang from Accept-Language header
|
|
376
410
|
const acceptLanguage = req.headers['accept-language'];
|
|
377
411
|
if (acceptLanguage) {
|
|
@@ -380,11 +414,6 @@ class DLServer {
|
|
|
380
414
|
return langCode;
|
|
381
415
|
}
|
|
382
416
|
}
|
|
383
|
-
// Try to get lang from body
|
|
384
|
-
const bodyLang = req.body?.lang;
|
|
385
|
-
if (bodyLang && i18n_js_1.LANG_CODES.has(bodyLang)) {
|
|
386
|
-
return bodyLang;
|
|
387
|
-
}
|
|
388
417
|
// Fallback to default
|
|
389
418
|
return (0, i18n_js_1.getLang)();
|
|
390
419
|
}
|
|
@@ -471,19 +500,32 @@ class DLServer {
|
|
|
471
500
|
// });
|
|
472
501
|
// API to start m3u8 download
|
|
473
502
|
app.post('/api/download', (req, res) => {
|
|
474
|
-
const {
|
|
503
|
+
const { list = [] } = req.body;
|
|
475
504
|
const lang = this.getLangFromRequest(req);
|
|
476
505
|
try {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
506
|
+
let duplicateCount = 0;
|
|
507
|
+
let startedCount = 0;
|
|
508
|
+
// 检查并统计重复的 URL,但仍允许下载
|
|
509
|
+
for (const item of list) {
|
|
510
|
+
const { url, ...options } = item;
|
|
511
|
+
if (url) {
|
|
512
|
+
if (this.dlCache.has(url)) {
|
|
513
|
+
duplicateCount++;
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
startedCount++;
|
|
517
|
+
}
|
|
518
|
+
this.startDownload(url, options);
|
|
482
519
|
}
|
|
483
520
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
521
|
+
let message = '';
|
|
522
|
+
if (duplicateCount > 0) {
|
|
523
|
+
message = `${(0, i18n_js_1.t)('api.success.downloadStarted', lang, { count: list.length })},${(0, i18n_js_1.t)('api.error.duplicateDownload', lang, { count: duplicateCount })}`;
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
message = (0, i18n_js_1.t)('api.success.downloadStarted', lang, { count: list.length });
|
|
527
|
+
}
|
|
528
|
+
res.json({ message, code: 0, duplicateCount, startedCount });
|
|
487
529
|
this.wsSend('tasks');
|
|
488
530
|
}
|
|
489
531
|
catch (error) {
|
|
@@ -536,7 +578,7 @@ class DLServer {
|
|
|
536
578
|
});
|
|
537
579
|
});
|
|
538
580
|
// API to delete download
|
|
539
|
-
app.post('/api/delete', (req, res) => {
|
|
581
|
+
app.post('/api/delete', async (req, res) => {
|
|
540
582
|
const { urls, deleteCache = false, deleteVideo = false } = req.body;
|
|
541
583
|
const urlsToDelete = urls;
|
|
542
584
|
const list = [];
|
|
@@ -546,21 +588,21 @@ class DLServer {
|
|
|
546
588
|
(0, m3u8_download_js_1.m3u8DLStop)(url, item.workPoll);
|
|
547
589
|
this.dlCache.delete(url);
|
|
548
590
|
list.push(item.url);
|
|
549
|
-
if (deleteCache
|
|
550
|
-
const cacheDir =
|
|
551
|
-
if ((0,
|
|
552
|
-
|
|
591
|
+
if (deleteCache) {
|
|
592
|
+
const cacheDir = item.cacheDir;
|
|
593
|
+
if (await (0, utils_js_1.checkFileExists)(cacheDir)) {
|
|
594
|
+
await node_fs_1.promises.rm(cacheDir, { recursive: true });
|
|
553
595
|
utils_js_1.logger.debug('删除缓存目录:', cacheDir);
|
|
554
596
|
}
|
|
555
597
|
}
|
|
556
598
|
if (deleteVideo) {
|
|
557
|
-
['.ts', '.mp4']
|
|
599
|
+
for (const ext of ['.ts', '.mp4']) {
|
|
558
600
|
const filepath = (0, node_path_1.resolve)(item.options.saveDir, item.options.filename + ext);
|
|
559
|
-
if ((0,
|
|
560
|
-
|
|
601
|
+
if (await (0, utils_js_1.checkFileExists)(filepath)) {
|
|
602
|
+
await node_fs_1.promises.unlink(filepath);
|
|
561
603
|
utils_js_1.logger.debug('删除文件:', filepath);
|
|
562
604
|
}
|
|
563
|
-
}
|
|
605
|
+
}
|
|
564
606
|
}
|
|
565
607
|
}
|
|
566
608
|
}
|
|
@@ -572,20 +614,73 @@ class DLServer {
|
|
|
572
614
|
const lang = this.getLangFromRequest(req);
|
|
573
615
|
res.json({ message: (0, i18n_js_1.t)('api.success.deleted', lang, { count: list.length }), code: 0, count: list.length });
|
|
574
616
|
});
|
|
575
|
-
|
|
617
|
+
// API to rename download file
|
|
618
|
+
app.post('/api/rename', async (req, res) => {
|
|
619
|
+
const { url, newFilename } = req.body;
|
|
620
|
+
const lang = this.getLangFromRequest(req);
|
|
621
|
+
if (!url || !newFilename) {
|
|
622
|
+
res.json({ code: 1001, message: (0, i18n_js_1.t)('api.error.invalidParams', lang) });
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
// 检查新文件名是否包含非法字符
|
|
626
|
+
const invalidChars = /[<>:"/\\|?*]/;
|
|
627
|
+
if (invalidChars.test(newFilename)) {
|
|
628
|
+
res.json({ code: 1004, message: (0, i18n_js_1.t)('api.error.invalidFilename', lang) });
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const item = this.dlCache.get(url);
|
|
632
|
+
if (!item) {
|
|
633
|
+
res.json({ code: 1002, message: (0, i18n_js_1.t)('api.error.taskNotFound', lang) });
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
await this.checkItemStatus(item);
|
|
637
|
+
// 只允许重命名已完成且状态正常的任务
|
|
638
|
+
if (item.status !== 'done' || item.errmsg) {
|
|
639
|
+
utils_js_1.logger.error('rename failed:', item.status, (0, console_log_colors_1.red)(item.errmsg), (0, console_log_colors_1.gray)(url));
|
|
640
|
+
res.json({ code: 1003, message: (0, i18n_js_1.t)('api.error.onlyRenameCompleted', lang) });
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
try {
|
|
644
|
+
const oldPath = item.localVideo;
|
|
645
|
+
const oldDir = (0, node_path_1.dirname)(oldPath);
|
|
646
|
+
const oldExt = oldPath.split('.').pop() || '';
|
|
647
|
+
const newFilenameBase = newFilename.replace(/\.[^.]+$/, '');
|
|
648
|
+
const newPath = (0, node_path_1.resolve)(oldDir, `${newFilenameBase}${oldExt ? `.${oldExt}` : ''}`);
|
|
649
|
+
// 检查新文件名是否已存在
|
|
650
|
+
if (await (0, utils_js_1.checkFileExists)(newPath)) {
|
|
651
|
+
res.json({ code: 1007, message: (0, i18n_js_1.t)('api.error.fileExists', lang) });
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
// 重命名文件
|
|
655
|
+
await node_fs_1.promises.rename(oldPath, newPath);
|
|
656
|
+
utils_js_1.logger.debug('重命名文件:', (0, console_log_colors_1.gray)(oldPath), '->', (0, console_log_colors_1.cyan)(newPath));
|
|
657
|
+
// 更新任务信息
|
|
658
|
+
item.localVideo = newPath;
|
|
659
|
+
item.options.filename = item.filename = (0, node_path_1.basename)(newPath);
|
|
660
|
+
this.dlCache.set(url, item);
|
|
661
|
+
this.saveCache();
|
|
662
|
+
this.wsSend('progress', url);
|
|
663
|
+
res.json({ message: (0, i18n_js_1.t)('api.success.renamed', lang), code: 0 });
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
utils_js_1.logger.error('重命名失败:', error);
|
|
667
|
+
res.json({ code: 1006, message: (0, i18n_js_1.t)('api.error.renameFailed', lang, { error: error.message }) });
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
app.get(/^\/localplay\/(.*)$/, async (req, res) => {
|
|
576
671
|
let filepath = decodeURIComponent(req.params[0]);
|
|
577
672
|
if (filepath) {
|
|
578
673
|
let ext = filepath.split('.').pop();
|
|
579
674
|
if (!ext) {
|
|
580
675
|
ext = 'm3u8';
|
|
581
|
-
if (!(0,
|
|
676
|
+
if (!(await (0, utils_js_1.checkFileExists)(filepath)))
|
|
582
677
|
filepath += '.m3u8';
|
|
583
678
|
}
|
|
584
679
|
const allowedDirs = [this.options.cacheDir, this.cfg.dlOptions.saveDir];
|
|
585
|
-
if (!(0,
|
|
680
|
+
if (!(await (0, utils_js_1.checkFileExists)(filepath))) {
|
|
586
681
|
for (const dir of allowedDirs) {
|
|
587
682
|
const tpath = (0, node_path_1.resolve)(dir, filepath);
|
|
588
|
-
if ((0,
|
|
683
|
+
if (await (0, utils_js_1.checkFileExists)(tpath)) {
|
|
589
684
|
filepath = tpath;
|
|
590
685
|
break;
|
|
591
686
|
}
|
|
@@ -601,8 +696,8 @@ class DLServer {
|
|
|
601
696
|
return;
|
|
602
697
|
}
|
|
603
698
|
}
|
|
604
|
-
if ((0,
|
|
605
|
-
const stats =
|
|
699
|
+
if (await (0, utils_js_1.checkFileExists)(filepath)) {
|
|
700
|
+
const stats = await node_fs_1.promises.stat(filepath);
|
|
606
701
|
const headers = new Headers({
|
|
607
702
|
'Last-Modified': stats.mtime.toUTCString(),
|
|
608
703
|
'Access-Control-Allow-Headers': '*',
|
|
@@ -614,7 +709,8 @@ class DLServer {
|
|
|
614
709
|
});
|
|
615
710
|
res.setHeaders(headers);
|
|
616
711
|
if (ext === 'm3u8' || ('ts' === ext && stats.size < 1024 * 1024 * 3)) {
|
|
617
|
-
|
|
712
|
+
const data = await node_fs_1.promises.readFile(filepath);
|
|
713
|
+
res.send(data);
|
|
618
714
|
utils_js_1.logger.debug('[Localplay]file sent:', (0, console_log_colors_1.gray)(filepath), 'Size:', stats.size, 'bytes');
|
|
619
715
|
}
|
|
620
716
|
else {
|