@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.
@@ -60,7 +60,7 @@ class DLServer {
60
60
  };
61
61
  serverInfo = {
62
62
  version: '',
63
- ariang: (0, node_fs_1.existsSync)((0, node_path_1.resolve)(rootDir, 'client/ariang/index.html')),
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
- this.readConfig();
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, node_fs_1.existsSync)(cacheFile)) {
115
- JSON.parse((0, node_fs_1.readFileSync)(cacheFile, 'utf8')).forEach(([url, item]) => {
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
- if (now - this.checkDLFileLaest < interval) {
130
- this.checkDLFileTimer = setTimeout(() => this.checkDLFileIsExists(), interval - (now - this.checkDLFileLaest));
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.forEach(item => {
134
- if (item.status === 'done' && (!item.localVideo || !(0, node_fs_1.existsSync)(item.localVideo))) {
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
- (0, node_fs_1.writeFileSync)(cacheFile, JSON.stringify(info));
178
+ node_fs_1.promises.writeFile(cacheFile, JSON.stringify(this.dlCacheClone()));
160
179
  }, 1000);
161
180
  }
162
- readConfig(configPath) {
181
+ async loadConfig(configPath) {
163
182
  try {
164
183
  if (!configPath)
165
184
  configPath = this.options.configPath;
166
- if ((0, node_fs_1.existsSync)(configPath))
167
- (0, fe_utils_1.assign)(this.cfg, JSON.parse((0, node_fs_1.readFileSync)(configPath, 'utf8')));
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('读取配置失败:', 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, node_fs_1.existsSync)(ffmpegPath)) {
198
+ if (!(await (0, utils_js_1.checkFileExists)(ffmpegPath))) {
181
199
  throw new Error(`ffmpeg 路径不存在: ${ffmpegPath}`);
182
200
  }
183
201
  // 检查是否为文件(不是目录)
184
- const stats = (0, node_fs_1.statSync)(ffmpegPath);
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
- (0, node_fs_1.writeFileSync)(configPath, JSON.stringify(this.cfg, null, 2));
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
- if (isIndexPage || isPlayPage) {
213
- const version = this.serverInfo.version;
214
- let htmlContent = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(rootDir, `client/${isPlayPage ? 'play' : 'index'}.html`), 'utf-8').replaceAll('{{version}}', version);
215
- if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(rootDir, 'client/local/cdn'))) {
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, node_fs_1.existsSync)(localPath)) {
224
- htmlContent = htmlContent.replaceAll(match, `local/cdn/${relativePath}`);
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 && !(0, node_fs_1.existsSync)(cacheItem.localVideo))
296
- delete cacheItem.localVideo;
297
- if (cacheItem.endTime)
298
- delete cacheItem.endTime;
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
- Object.assign(item, { ...stats, current, options: dlOptions, status, workPoll, url });
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, node_fs_1.existsSync)(r.filepath)) {
358
+ if (r.filepath && (await (0, utils_js_1.checkFileExists)(r.filepath))) {
329
359
  item.localVideo = r.filepath;
330
- item.downloadedSize = (0, node_fs_1.statSync)(r.filepath).size;
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 { url, options = {}, list = [] } = req.body;
503
+ const { list = [] } = req.body;
474
504
  const lang = this.getLangFromRequest(req);
475
505
  try {
476
- if (list.length) {
477
- for (const item of list) {
478
- const { url, ...options } = item;
479
- if (url)
480
- this.startDownload(url, options);
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
- else if (url)
484
- this.startDownload(url, options);
485
- res.json({ message: (0, i18n_js_1.t)('api.success.downloadStarted', lang, { count: list.length || 1 }), code: 0 });
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 && item.current?.tsOut) {
549
- const cacheDir = (0, node_path_1.dirname)(item.current.tsOut);
550
- if ((0, node_fs_1.existsSync)(cacheDir)) {
551
- (0, node_fs_1.rmSync)(cacheDir, { recursive: true });
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'].forEach(ext => {
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, node_fs_1.existsSync)(filepath)) {
559
- (0, node_fs_1.unlinkSync)(filepath);
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
- app.get(/^\/localplay\/(.*)$/, (req, res) => {
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, node_fs_1.existsSync)(filepath))
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, node_fs_1.existsSync)(filepath)) {
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, node_fs_1.existsSync)(tpath)) {
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, node_fs_1.existsSync)(filepath)) {
604
- const stats = (0, node_fs_1.statSync)(filepath);
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
- res.send((0, node_fs_1.readFileSync)(filepath));
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 {
@@ -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
  /** 下载进度统计 */