@lzwme/m3u8-dl 1.6.0-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 +293 -212
- package/README.zh-CN.md +580 -0
- package/cjs/i18n/locales/en.d.ts +9 -0
- package/cjs/i18n/locales/en.js +10 -1
- package/cjs/i18n/locales/{zh.d.ts → zh-CN.d.ts} +9 -0
- package/cjs/i18n/locales/{zh.js → zh-CN.js} +10 -1
- package/cjs/lib/file-download.js +1 -1
- package/cjs/lib/format-options.js +27 -4
- package/cjs/lib/i18n.d.ts +1 -1
- package/cjs/lib/i18n.js +29 -10
- package/cjs/lib/local-play.js +1 -1
- package/cjs/lib/m3u8-download.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 +170 -73
- package/cjs/types/m3u8.d.ts +2 -2
- package/client/assets/main-BSWj1VKy.js +29 -0
- package/client/assets/main-T6xR17Gh.css +1 -0
- package/client/index.html +3 -3
- package/client/m3u8-capture.user.js +114 -0
- package/client/play.html +38 -21
- package/package.json +27 -13
- package/client/assets/main-DYJAIw1q.css +0 -1
- package/client/assets/main-XL0wiaDU.js +0 -25
|
@@ -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,16 +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
|
-
|
|
213
|
-
|
|
214
|
-
let htmlContent =
|
|
215
|
-
if (
|
|
231
|
+
const isApi = req.path.startsWith('/api/');
|
|
232
|
+
if (!isApi && (isIndexPage || isPlayPage)) {
|
|
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) {
|
|
216
235
|
// 提取所有 zstatic.net 的 js 和 css 资源地址,若子路径存在于 local/cdn 目录下则替换为本地路径
|
|
217
236
|
const zstaticRegex = /https:\/\/s4\.zstatic\.net\/ajax\/libs\/[^\s"'`<>]+\.(js|css)/g;
|
|
218
237
|
const zstaticMatches = htmlContent.match(zstaticRegex);
|
|
@@ -220,8 +239,8 @@ class DLServer {
|
|
|
220
239
|
for (const match of zstaticMatches) {
|
|
221
240
|
const relativePath = match.split('libs/')[1];
|
|
222
241
|
const localPath = (0, node_path_1.resolve)(rootDir, `client/local/cdn/${relativePath}`);
|
|
223
|
-
if ((0,
|
|
224
|
-
htmlContent = htmlContent.replaceAll(match,
|
|
242
|
+
if (await (0, utils_js_1.checkFileExists)(localPath)) {
|
|
243
|
+
htmlContent = htmlContent.replaceAll(match, `/local/cdn/${relativePath}`);
|
|
225
244
|
}
|
|
226
245
|
}
|
|
227
246
|
htmlContent = htmlContent.replaceAll(/integrity="[^"]+"\n?/g, '');
|
|
@@ -288,14 +307,23 @@ class DLServer {
|
|
|
288
307
|
options.filename = item[1];
|
|
289
308
|
}
|
|
290
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;
|
|
291
312
|
const cacheItem = this.dlCache.get(url) || { options, dlOptions, status: 'pending', url };
|
|
292
313
|
utils_js_1.logger.debug('startDownload', url, dlOptions, cacheItem.status);
|
|
293
314
|
if (cacheItem.status === 'resume')
|
|
294
315
|
return;
|
|
295
|
-
if (cacheItem.localVideo
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
+
}
|
|
299
327
|
cacheItem.status = this.downloading >= this.cfg.webOptions.maxDownloads ? 'pending' : 'resume';
|
|
300
328
|
// pending 优先级靠后
|
|
301
329
|
if (cacheItem.status === 'pending' && this.dlCache.has(url))
|
|
@@ -316,18 +344,20 @@ class DLServer {
|
|
|
316
344
|
if (!item)
|
|
317
345
|
return false; // 已删除
|
|
318
346
|
const status = item.status || 'resume';
|
|
319
|
-
|
|
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 });
|
|
320
350
|
this.dlCache.set(url, item);
|
|
321
351
|
this.saveCache();
|
|
322
352
|
this.wsSend('progress', url);
|
|
323
353
|
return status !== 'pause';
|
|
324
354
|
},
|
|
325
355
|
};
|
|
326
|
-
const afterDownload = (r, url) => {
|
|
356
|
+
const afterDownload = async (r, url) => {
|
|
327
357
|
const item = this.dlCache.get(url) || cacheItem;
|
|
328
|
-
if (r.filepath && (0,
|
|
358
|
+
if (r.filepath && (await (0, utils_js_1.checkFileExists)(r.filepath))) {
|
|
329
359
|
item.localVideo = r.filepath;
|
|
330
|
-
item.downloadedSize = (
|
|
360
|
+
item.downloadedSize = (await node_fs_1.promises.stat(r.filepath)).size;
|
|
331
361
|
}
|
|
332
362
|
else if (!r.errmsg && opts.convert !== false)
|
|
333
363
|
r.errmsg = '下载失败';
|
|
@@ -371,6 +401,11 @@ class DLServer {
|
|
|
371
401
|
if (queryLang && i18n_js_1.LANG_CODES.has(queryLang)) {
|
|
372
402
|
return queryLang;
|
|
373
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
|
+
}
|
|
374
409
|
// Try to get lang from Accept-Language header
|
|
375
410
|
const acceptLanguage = req.headers['accept-language'];
|
|
376
411
|
if (acceptLanguage) {
|
|
@@ -379,11 +414,6 @@ class DLServer {
|
|
|
379
414
|
return langCode;
|
|
380
415
|
}
|
|
381
416
|
}
|
|
382
|
-
// Try to get lang from body
|
|
383
|
-
const bodyLang = req.body?.lang;
|
|
384
|
-
if (bodyLang && i18n_js_1.LANG_CODES.has(bodyLang)) {
|
|
385
|
-
return bodyLang;
|
|
386
|
-
}
|
|
387
417
|
// Fallback to default
|
|
388
418
|
return (0, i18n_js_1.getLang)();
|
|
389
419
|
}
|
|
@@ -470,19 +500,32 @@ class DLServer {
|
|
|
470
500
|
// });
|
|
471
501
|
// API to start m3u8 download
|
|
472
502
|
app.post('/api/download', (req, res) => {
|
|
473
|
-
const {
|
|
503
|
+
const { list = [] } = req.body;
|
|
474
504
|
const lang = this.getLangFromRequest(req);
|
|
475
505
|
try {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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);
|
|
481
519
|
}
|
|
482
520
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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 });
|
|
486
529
|
this.wsSend('tasks');
|
|
487
530
|
}
|
|
488
531
|
catch (error) {
|
|
@@ -535,7 +578,7 @@ class DLServer {
|
|
|
535
578
|
});
|
|
536
579
|
});
|
|
537
580
|
// API to delete download
|
|
538
|
-
app.post('/api/delete', (req, res) => {
|
|
581
|
+
app.post('/api/delete', async (req, res) => {
|
|
539
582
|
const { urls, deleteCache = false, deleteVideo = false } = req.body;
|
|
540
583
|
const urlsToDelete = urls;
|
|
541
584
|
const list = [];
|
|
@@ -545,21 +588,21 @@ class DLServer {
|
|
|
545
588
|
(0, m3u8_download_js_1.m3u8DLStop)(url, item.workPoll);
|
|
546
589
|
this.dlCache.delete(url);
|
|
547
590
|
list.push(item.url);
|
|
548
|
-
if (deleteCache
|
|
549
|
-
const cacheDir =
|
|
550
|
-
if ((0,
|
|
551
|
-
|
|
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 });
|
|
552
595
|
utils_js_1.logger.debug('删除缓存目录:', cacheDir);
|
|
553
596
|
}
|
|
554
597
|
}
|
|
555
598
|
if (deleteVideo) {
|
|
556
|
-
['.ts', '.mp4']
|
|
599
|
+
for (const ext of ['.ts', '.mp4']) {
|
|
557
600
|
const filepath = (0, node_path_1.resolve)(item.options.saveDir, item.options.filename + ext);
|
|
558
|
-
if ((0,
|
|
559
|
-
|
|
601
|
+
if (await (0, utils_js_1.checkFileExists)(filepath)) {
|
|
602
|
+
await node_fs_1.promises.unlink(filepath);
|
|
560
603
|
utils_js_1.logger.debug('删除文件:', filepath);
|
|
561
604
|
}
|
|
562
|
-
}
|
|
605
|
+
}
|
|
563
606
|
}
|
|
564
607
|
}
|
|
565
608
|
}
|
|
@@ -571,20 +614,73 @@ class DLServer {
|
|
|
571
614
|
const lang = this.getLangFromRequest(req);
|
|
572
615
|
res.json({ message: (0, i18n_js_1.t)('api.success.deleted', lang, { count: list.length }), code: 0, count: list.length });
|
|
573
616
|
});
|
|
574
|
-
|
|
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) => {
|
|
575
671
|
let filepath = decodeURIComponent(req.params[0]);
|
|
576
672
|
if (filepath) {
|
|
577
673
|
let ext = filepath.split('.').pop();
|
|
578
674
|
if (!ext) {
|
|
579
675
|
ext = 'm3u8';
|
|
580
|
-
if (!(0,
|
|
676
|
+
if (!(await (0, utils_js_1.checkFileExists)(filepath)))
|
|
581
677
|
filepath += '.m3u8';
|
|
582
678
|
}
|
|
583
679
|
const allowedDirs = [this.options.cacheDir, this.cfg.dlOptions.saveDir];
|
|
584
|
-
if (!(0,
|
|
680
|
+
if (!(await (0, utils_js_1.checkFileExists)(filepath))) {
|
|
585
681
|
for (const dir of allowedDirs) {
|
|
586
682
|
const tpath = (0, node_path_1.resolve)(dir, filepath);
|
|
587
|
-
if ((0,
|
|
683
|
+
if (await (0, utils_js_1.checkFileExists)(tpath)) {
|
|
588
684
|
filepath = tpath;
|
|
589
685
|
break;
|
|
590
686
|
}
|
|
@@ -600,8 +696,8 @@ class DLServer {
|
|
|
600
696
|
return;
|
|
601
697
|
}
|
|
602
698
|
}
|
|
603
|
-
if ((0,
|
|
604
|
-
const stats =
|
|
699
|
+
if (await (0, utils_js_1.checkFileExists)(filepath)) {
|
|
700
|
+
const stats = await node_fs_1.promises.stat(filepath);
|
|
605
701
|
const headers = new Headers({
|
|
606
702
|
'Last-Modified': stats.mtime.toUTCString(),
|
|
607
703
|
'Access-Control-Allow-Headers': '*',
|
|
@@ -613,7 +709,8 @@ class DLServer {
|
|
|
613
709
|
});
|
|
614
710
|
res.setHeaders(headers);
|
|
615
711
|
if (ext === 'm3u8' || ('ts' === ext && stats.size < 1024 * 1024 * 3)) {
|
|
616
|
-
|
|
712
|
+
const data = await node_fs_1.promises.readFile(filepath);
|
|
713
|
+
res.send(data);
|
|
617
714
|
utils_js_1.logger.debug('[Localplay]file sent:', (0, console_log_colors_1.gray)(filepath), 'Size:', stats.size, 'bytes');
|
|
618
715
|
}
|
|
619
716
|
else {
|
package/cjs/types/m3u8.d.ts
CHANGED
|
@@ -142,8 +142,8 @@ export interface M3u8DLOptions {
|
|
|
142
142
|
type?: 'm3u8' | 'file' | 'parser';
|
|
143
143
|
/** ffmpeg 可执行文件路径。如果未指定,则尝试使用系统 PATH 中的 'ffmpeg' */
|
|
144
144
|
ffmpegPath?: string;
|
|
145
|
-
/** 语言。可选值:zh, en */
|
|
146
|
-
lang?: 'zh' | 'en';
|
|
145
|
+
/** 语言。可选值:zh-CN, en */
|
|
146
|
+
lang?: 'zh-CN' | 'en';
|
|
147
147
|
}
|
|
148
148
|
export interface M3u8DLResult extends Partial<DownloadResult> {
|
|
149
149
|
/** 下载进度统计 */
|