@lzwme/m3u8-dl 1.7.0 → 1.8.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/cjs/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './lib/file-download';
2
2
  export * from './lib/getM3u8Urls';
3
+ export * from './lib/init-proxy';
3
4
  export * from './lib/m3u8-download';
4
5
  export * from './lib/parseM3u8';
5
6
  export * from './video-parser';
package/cjs/index.js CHANGED
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./lib/file-download"), exports);
18
18
  __exportStar(require("./lib/getM3u8Urls"), exports);
19
+ __exportStar(require("./lib/init-proxy"), exports);
19
20
  __exportStar(require("./lib/m3u8-download"), exports);
20
21
  __exportStar(require("./lib/parseM3u8"), exports);
21
22
  __exportStar(require("./video-parser"), exports);
@@ -9,7 +9,7 @@ const i18n_js_1 = require("./i18n.js");
9
9
  const utils_js_1 = require("./utils.js");
10
10
  async function fileDownload(u, opts) {
11
11
  utils_js_1.logger.debug('fileDownload', u, opts);
12
- const { url, options } = (0, format_options_js_1.formatOptions)(u, opts);
12
+ const { url, options } = await (0, format_options_js_1.formatOptions)(u, opts);
13
13
  const lang = (0, i18n_js_1.getLang)(options.lang);
14
14
  const startTime = Date.now();
15
15
  const stats = {
@@ -68,7 +68,7 @@ async function fileDownload(u, opts) {
68
68
  });
69
69
  stats.endTime = Date.now();
70
70
  return {
71
- errmsg: r.filepath ? (0, i18n_js_1.t)('download.status.completed', lang) : (0, i18n_js_1.t)('download.status.failed', lang),
71
+ errmsg: r.filepath ? '' : (0, i18n_js_1.t)('download.status.failed', lang), // t('download.status.completed', lang)
72
72
  ...r,
73
73
  stats,
74
74
  };
@@ -1,6 +1,6 @@
1
1
  import type { M3u8DLOptions } from '../types';
2
- export declare function formatOptions(url: string, opts: M3u8DLOptions): {
2
+ export declare function formatOptions(url: string, opts: M3u8DLOptions): Promise<{
3
3
  url: string;
4
4
  options: M3u8DLOptions;
5
5
  urlMd5: string;
6
- };
6
+ }>;
@@ -47,7 +47,7 @@ const fileSupportExtList = [
47
47
  '.deb',
48
48
  '.rpm',
49
49
  ];
50
- function formatOptions(url, opts) {
50
+ async function formatOptions(url, opts) {
51
51
  const options = {
52
52
  delCache: !opts.debug,
53
53
  saveDir: process.cwd(),
@@ -58,6 +58,18 @@ function formatOptions(url, opts) {
58
58
  if (!options.type) {
59
59
  if (video_parser_1.VideoParser.getPlatform(url).platform !== 'unknown') {
60
60
  options.type = 'parser';
61
+ if (!opts.filename) {
62
+ const info = await video_parser_1.VideoParser.parse(url);
63
+ if (info.code === 0 && info.data?.title) {
64
+ options.filename = info.data.title
65
+ .split('\n')[0]
66
+ // 替换全部的非中英文、数字、下划线为下划线
67
+ .replace(/[^\u4e00-\u9fa5a-zA-Z0-9]+/g, '_')
68
+ .trim()
69
+ .replace(/_+/g, '_')
70
+ .slice(0, 100);
71
+ }
72
+ }
61
73
  }
62
74
  else {
63
75
  options.type = 'm3u8';
@@ -0,0 +1,5 @@
1
+ import type { M3u8DLOptions } from '../types';
2
+ /**
3
+ * 初始化代理
4
+ */
5
+ export declare function initProxy(options: M3u8DLOptions): Promise<void>;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.initProxy = initProxy;
37
+ const utils_1 = require("./utils");
38
+ /** 初始化失败 N 次后续忽略代理配置 */
39
+ let initializFailedTimes = 3;
40
+ /**
41
+ * 初始化代理
42
+ */
43
+ async function initProxy(options) {
44
+ if (!initializFailedTimes && !options.force)
45
+ return;
46
+ // 根据代理模式来初始化
47
+ try {
48
+ const g = global;
49
+ let globalAgent = g.GLOBAL_AGENT;
50
+ if (!globalAgent) {
51
+ // 代理未初始化且为禁用状态,则直接返回
52
+ if (options.proxyMode === 'disabled')
53
+ return;
54
+ // 为系统代理模式,但未设置环境变量,则直接返回
55
+ if (options.proxyMode === 'system' && !process.env.HTTP_PROXY && !process.env.HTTPS_PROXY)
56
+ return;
57
+ const globalAgentModule = await Promise.resolve().then(() => __importStar(require('global-agent')));
58
+ const ok = globalAgentModule.bootstrap();
59
+ if (ok)
60
+ globalAgent = g.GLOBAL_AGENT;
61
+ }
62
+ if (options.proxyMode !== 'disabled' && options.noProxy) {
63
+ options.noProxy = options.noProxy.replaceAll('\n', ',').trim();
64
+ }
65
+ if (options.proxyMode === 'custom' && options.proxyUrl) {
66
+ // 自定义代理模式
67
+ globalAgent.HTTP_PROXY = options.proxyUrl;
68
+ globalAgent.HTTPS_PROXY = options.proxyUrl;
69
+ globalAgent.NO_PROXY = options.noProxy;
70
+ utils_1.logger.info('Custom proxy enabled:', options.proxyUrl);
71
+ }
72
+ else if (options.proxyMode === 'disabled') {
73
+ globalAgent.HTTP_PROXY = undefined;
74
+ globalAgent.HTTPS_PROXY = undefined;
75
+ globalAgent.NO_PROXY = undefined;
76
+ // 关闭代理
77
+ utils_1.logger.info('Proxy disabled');
78
+ }
79
+ else {
80
+ // } else if (options.proxyMode === 'system') {
81
+ // 默认为使用系统代理,但支持自定义代理过滤
82
+ globalAgent.HTTP_PROXY = process.env.HTTP_PROXY;
83
+ globalAgent.HTTPS_PROXY = process.env.HTTPS_PROXY;
84
+ globalAgent.NO_PROXY = options.noProxy || process.env.NO_PROXY;
85
+ utils_1.logger.info('System proxy enabled');
86
+ }
87
+ }
88
+ catch (error) {
89
+ utils_1.logger.error('Failed to initialize proxy:', error);
90
+ utils_1.logger.warn('Please install global-agent to enable proxy support: npm install global-agent');
91
+ initializFailedTimes--;
92
+ }
93
+ }
@@ -5,6 +5,6 @@ import type { TsItemInfo } from '../types/m3u8.js';
5
5
  export declare function localPlay(m3u8Info: TsItemInfo[]): Promise<{
6
6
  port: number;
7
7
  origin: string;
8
- server: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
8
+ server: import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>;
9
9
  }>;
10
10
  export declare function toLocalM3u8(m3u8Info: TsItemInfo[], m3u8FilePath?: string, host?: string): string;
@@ -22,15 +22,9 @@ async function m3u8Convert(options, data) {
22
22
  if (process.platform === 'win32')
23
23
  filesAllArr = filesAllArr.map(d => d.replaceAll('\\', '/'));
24
24
  (0, node_fs_1.writeFileSync)(ffconcatFile, `ffconcat version 1.0\n${filesAllArr.join('\n')}`);
25
- let headersString = '';
26
- if (options.headers) {
27
- for (const [key, value] of Object.entries(options.headers)) {
28
- headersString += `-headers "${key}: ${String(value)}" `;
29
- }
30
- }
31
25
  // ffmpeg -i nz.ts -c copy -map 0:v -map 0:a -bsf:a aac_adtstoasc nz.mp4
32
- // const cmd = `"${ffmpegBin}" -async 1 -y -f concat -safe 0 -i "${ffconcatFile}" -acodec copy -vcodec copy -bsf:a aac_adtstoasc ${headersString} "${filepath}"`;
33
- const cmd = `"${ffmpegBin}" -async 1 -y -f concat -safe 0 -i "${ffconcatFile}" -c:v copy -c:a copy -movflags +faststart -fflags +genpts -bsf:a aac_adtstoasc ${headersString} "${filepath}"`;
26
+ // const cmd = `"${ffmpegBin}" -async 1 -y -f concat -safe 0 -i "${ffconcatFile}" -acodec copy -vcodec copy -bsf:a aac_adtstoasc "${filepath}"`;
27
+ const cmd = `"${ffmpegBin}" -async 1 -y -f concat -safe 0 -i "${ffconcatFile}" -c:v copy -c:a copy -movflags +faststart -fflags +genpts -bsf:a aac_adtstoasc "${filepath}"`;
34
28
  utils_1.logger.debug('[convert to mp4]cmd:', (0, console_log_colors_1.cyan)(cmd));
35
29
  const r = (0, fe_utils_1.execSync)(cmd);
36
30
  ffmpegSupport = !r.error;
@@ -90,7 +90,7 @@ exports.workPollPublic = new worker_pool_js_1.WorkerPool(tsDlFile);
90
90
  async function m3u8InfoParse(u, o = {}) {
91
91
  const ffmpegBin = o.ffmpegPath || 'ffmpeg';
92
92
  const ext = (0, utils_js_1.isSupportFfmpeg)(ffmpegBin) ? '.mp4' : '.ts';
93
- const { url, options, urlMd5 } = (0, format_options_js_1.formatOptions)(u, o);
93
+ const { url, options, urlMd5 } = await (0, format_options_js_1.formatOptions)(u, o);
94
94
  /** 最终合并转换后的文件路径 */
95
95
  let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename);
96
96
  if (!filepath.endsWith(ext))
@@ -104,8 +104,9 @@ async function m3u8InfoParse(u, o = {}) {
104
104
  return result;
105
105
  const lang = (0, i18n_js_1.getLang)(o.lang);
106
106
  const m3u8Info = await (0, parseM3u8_js_1.parseM3U8)(url, (0, node_path_1.resolve)(options.cacheDir, urlMd5), options.headers).catch(e => {
107
- utils_js_1.logger.error((0, i18n_js_1.t)('download.status.parseFailed', lang), e.message);
108
- console.log(e);
107
+ utils_js_1.logger.error((0, i18n_js_1.t)('download.status.parseFailed', lang), e?.message);
108
+ if (e)
109
+ console.log(e);
109
110
  });
110
111
  if (m3u8Info && m3u8Info?.tsCount > 0) {
111
112
  result.m3u8Info = m3u8Info;
@@ -66,7 +66,7 @@ async function parseM3U8(content, cacheDir = './cache', headers) {
66
66
  if (!tsKeyInfo.uri.includes('://'))
67
67
  tsKeyInfo.uri = new URL(tsKeyInfo.uri, url).toString();
68
68
  if (tsKeyInfo?.uri && !result.crypto[tsKeyInfo.uri]) {
69
- const r = await (0, utils_1.getRetry)(tsKeyInfo.uri);
69
+ const r = await (0, utils_1.getRetry)(tsKeyInfo.uri, headers);
70
70
  if (r.response.statusCode !== 200) {
71
71
  utils_1.logger.error('获取加密 key 失败:', tsKeyInfo.uri, r.response.statusCode, r.data);
72
72
  }
@@ -5,8 +5,8 @@ export declare const request: Request;
5
5
  export declare const getRetry: <T = string>(url: string, headers?: OutgoingHttpHeaders | string, retries?: number) => Promise<{
6
6
  data: T;
7
7
  buffer: Buffer;
8
- headers: import("http").IncomingHttpHeaders;
9
- response: import("http").IncomingMessage;
8
+ headers: import("node:http").IncomingHttpHeaders;
9
+ response: import("node:http").IncomingMessage;
10
10
  }>;
11
11
  export declare const logger: NLogger;
12
12
  export declare function isSupportFfmpeg(ffmpegBin: string): boolean;
package/cjs/lib/utils.js CHANGED
@@ -63,8 +63,29 @@ async function getLocation(url, method = 'HEAD') {
63
63
  function formatHeaders(headers) {
64
64
  if (!headers)
65
65
  return {};
66
- if (typeof headers === 'string')
67
- headers = Object.fromEntries(headers.split('\n').map(line => line.split(':').map(d => d.trim())));
66
+ if (typeof headers === 'string') {
67
+ headers = headers.trim();
68
+ if (headers.startsWith('{') && headers.endsWith('}')) {
69
+ try {
70
+ headers = JSON.parse(headers);
71
+ }
72
+ catch (e) {
73
+ console.error('解析 headers 失败:', e);
74
+ }
75
+ }
76
+ if (typeof headers === 'string') {
77
+ const parsed = {};
78
+ headers
79
+ .replace(/,\s*([a-zA-Z0-9_-]+:)/g, '\n$1') // 支持如 "Key1: Val1, Key2: Val2" 的格式
80
+ .split('\n')
81
+ .forEach(line => {
82
+ const idx = line.indexOf(':');
83
+ if (idx > 0)
84
+ parsed[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
85
+ });
86
+ headers = parsed;
87
+ }
88
+ }
68
89
  return (0, fe_utils_1.toLowcaseKeyObject)(headers);
69
90
  }
70
91
  /** 异步检查文件是否存在 */
@@ -75,7 +96,8 @@ async function checkFileExists(filepath) {
75
96
  await (0, promises_1.access)(filepath, promises_1.constants.F_OK);
76
97
  return true;
77
98
  }
78
- catch {
99
+ catch (error) {
100
+ exports.logger.debug('checkFileExists failed:', filepath, error.message);
79
101
  return false;
80
102
  }
81
103
  }
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from 'node:events';
2
- type WorkerPoolCallback<R> = (err: Error | null, result: R, startTime: number) => void;
2
+ type WorkerPoolCallback<R> = (err: Error, result: R, startTime: number) => void;
3
3
  export declare class WorkerPool<T = unknown, R = unknown> extends EventEmitter {
4
4
  private processorFile;
5
5
  numThreads: number;
@@ -38,7 +38,7 @@ async function formatUrls(urls, options) {
38
38
  continue;
39
39
  }
40
40
  }
41
- const r = (0, format_options_1.formatOptions)(url, options);
41
+ const r = await (0, format_options_1.formatOptions)(url, options);
42
42
  taskset.set(r.url, r.options);
43
43
  }
44
44
  return taskset;
@@ -88,8 +88,7 @@ async function m3u8BatchDownload(urls, options) {
88
88
  }
89
89
  };
90
90
  if (o.type === 'parser') {
91
- const vp = new video_parser_1.VideoParser();
92
- vp.download(url, o).then(r => afterDownload(r, url));
91
+ video_parser_1.VideoParser.download(url, o).then(r => afterDownload(r, url));
93
92
  }
94
93
  else if (o.type === 'file') {
95
94
  (0, file_download_1.fileDownload)(url, o).then(r => afterDownload(r, url));
@@ -43,6 +43,7 @@ const file_download_js_1 = require("../lib/file-download.js");
43
43
  const format_options_js_1 = require("../lib/format-options.js");
44
44
  const getM3u8Urls_js_1 = require("../lib/getM3u8Urls.js");
45
45
  const i18n_js_1 = require("../lib/i18n.js");
46
+ const init_proxy_js_1 = require("../lib/init-proxy.js");
46
47
  const m3u8_download_js_1 = require("../lib/m3u8-download.js");
47
48
  const utils_js_1 = require("../lib/utils.js");
48
49
  const index_js_1 = require("../video-parser/index.js");
@@ -78,6 +79,10 @@ class DLServer {
78
79
  saveDir: process.env.DS_SAVE_DIR || './downloads',
79
80
  threadNum: 4,
80
81
  ffmpegPath: process.env.DS_FFMPEG_PATH || undefined,
82
+ // 代理配置改为字符串模式:'custom', 'system', 'disabled'
83
+ proxyMode: process.env.DS_PROXY_MODE || 'system',
84
+ proxyUrl: process.env.DS_PROXY_URL || undefined,
85
+ noProxy: process.env.DS_NO_PROXY || undefined,
81
86
  },
82
87
  };
83
88
  /** 下载任务缓存 */
@@ -109,6 +114,8 @@ class DLServer {
109
114
  await this.createApp();
110
115
  this.initRouters();
111
116
  utils_js_1.logger.debug('Server initialized', 'cacheSize:', this.dlCache.size, this.options, this.cfg.dlOptions);
117
+ // 初始化 global-agent 代理
118
+ (0, init_proxy_js_1.initProxy)(this.cfg.dlOptions);
112
119
  }
113
120
  async loadCache() {
114
121
  const cacheFile = (0, node_path_1.resolve)(this.options.cacheDir, 'cache.json');
@@ -213,7 +220,10 @@ class DLServer {
213
220
  this.cfg.dlOptions[key] = value;
214
221
  }
215
222
  (0, fe_utils_1.mkdirp)((0, node_path_1.dirname)(configPath));
216
- return node_fs_1.promises.writeFile(configPath, JSON.stringify(this.cfg, null, 2));
223
+ const result = await node_fs_1.promises.writeFile(configPath, JSON.stringify(this.cfg, null, 2));
224
+ // 重新初始化代理
225
+ await (0, init_proxy_js_1.initProxy)(this.cfg.dlOptions);
226
+ return result;
217
227
  }
218
228
  async createApp() {
219
229
  const { default: express } = await Promise.resolve().then(() => __importStar(require('express')));
@@ -306,7 +316,7 @@ class DLServer {
306
316
  if (!options.filename)
307
317
  options.filename = item[1];
308
318
  }
309
- const { options: dlOptions } = (0, format_options_js_1.formatOptions)(url, { ...this.cfg.dlOptions, ...options, cacheDir: this.options.cacheDir });
319
+ const { options: dlOptions } = await (0, format_options_js_1.formatOptions)(url, { ...this.cfg.dlOptions, ...options, cacheDir: this.options.cacheDir });
310
320
  if (!dlOptions.saveDir)
311
321
  dlOptions.saveDir = this.cfg.dlOptions.saveDir;
312
322
  const cacheItem = this.dlCache.get(url) || { options, dlOptions, status: 'pending', url };
@@ -324,6 +334,16 @@ class DLServer {
324
334
  delete cacheItem.endTime;
325
335
  }
326
336
  }
337
+ else if (!this.dlCache.has(url)) {
338
+ // 如果本地视频已存在,则重命名 filename
339
+ const localVideo = (0, node_path_1.resolve)(dlOptions.saveDir, dlOptions.filename);
340
+ const hasSameNameVideo = [...this.dlCache.values()].some(d => d.dlOptions.saveDir === dlOptions.saveDir && d.dlOptions.filename === dlOptions.filename);
341
+ if (hasSameNameVideo || (await (0, utils_js_1.checkFileExists)(localVideo))) {
342
+ const ext = (0, node_path_1.extname)(localVideo) || '';
343
+ dlOptions.filename = `${(0, node_path_1.basename)(localVideo, ext)}.${Date.now()}${ext}`;
344
+ utils_js_1.logger.info('存在重名视频,重命名filename:', (0, console_log_colors_1.gray)(localVideo), '->', (0, console_log_colors_1.cyan)(dlOptions.filename));
345
+ }
346
+ }
327
347
  cacheItem.status = this.downloading >= this.cfg.webOptions.maxDownloads ? 'pending' : 'resume';
328
348
  // pending 优先级靠后
329
349
  if (cacheItem.status === 'pending' && this.dlCache.has(url))
@@ -363,7 +383,7 @@ class DLServer {
363
383
  r.errmsg = '下载失败';
364
384
  item.endTime = Date.now();
365
385
  item.errmsg = r.errmsg;
366
- item.status = r.errmsg ? 'error' : 'done';
386
+ item.status = r.isExist ? 'done' : r.errmsg ? 'error' : 'done';
367
387
  utils_js_1.logger.info('Download complete:', item.status, (0, console_log_colors_1.red)(r.errmsg), (0, console_log_colors_1.gray)(url), (0, console_log_colors_1.cyan)(r.filepath));
368
388
  this.dlCache.set(url, item);
369
389
  this.wsSend('progress', url);
@@ -372,8 +392,8 @@ class DLServer {
372
392
  };
373
393
  try {
374
394
  if (dlOptions.type === 'parser') {
375
- const vp = new index_js_1.VideoParser();
376
- vp.download(url, opts).then(r => afterDownload(r, url));
395
+ console.log('\n\nDownloading with VideoParser\n\n', dlOptions, url);
396
+ index_js_1.VideoParser.download(url, opts).then(r => afterDownload(r, url));
377
397
  }
378
398
  else if (dlOptions.type === 'file') {
379
399
  (0, file_download_js_1.fileDownload)(url, opts).then(r => afterDownload(r, url));
@@ -582,27 +602,80 @@ class DLServer {
582
602
  const { urls, deleteCache = false, deleteVideo = false } = req.body;
583
603
  const urlsToDelete = urls;
584
604
  const list = [];
605
+ const errors = [];
585
606
  for (const url of urlsToDelete) {
586
607
  const item = this.dlCache.get(url);
587
608
  if (item) {
609
+ utils_js_1.logger.info('delete download task:', (0, console_log_colors_1.gray)(url), (0, console_log_colors_1.cyan)(item.status), (0, console_log_colors_1.cyan)(item.localVideo), deleteCache, deleteVideo);
588
610
  (0, m3u8_download_js_1.m3u8DLStop)(url, item.workPoll);
589
611
  this.dlCache.delete(url);
590
612
  list.push(item.url);
591
613
  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 });
595
- utils_js_1.logger.debug('删除缓存目录:', cacheDir);
614
+ try {
615
+ const cacheDir = item.cacheDir;
616
+ if (cacheDir && (await (0, utils_js_1.checkFileExists)(cacheDir))) {
617
+ await node_fs_1.promises.rm(cacheDir, { recursive: true, force: true });
618
+ utils_js_1.logger.info('删除缓存目录:', (0, console_log_colors_1.gray)(cacheDir));
619
+ }
620
+ }
621
+ catch (error) {
622
+ const errorMsg = `删除缓存目录失败: ${error.message}`;
623
+ utils_js_1.logger.error(errorMsg, (0, console_log_colors_1.gray)(item.cacheDir));
624
+ errors.push(errorMsg);
596
625
  }
597
626
  }
598
627
  if (deleteVideo) {
599
- for (const ext of ['.ts', '.mp4']) {
600
- const filepath = (0, node_path_1.resolve)(item.options.saveDir, item.options.filename + ext);
601
- if (await (0, utils_js_1.checkFileExists)(filepath)) {
602
- await node_fs_1.promises.unlink(filepath);
603
- utils_js_1.logger.debug('删除文件:', filepath);
628
+ try {
629
+ // 优先使用 item.localVideo(实际文件路径)
630
+ if (item.localVideo) {
631
+ const filepath = item.localVideo;
632
+ if (await (0, utils_js_1.checkFileExists)(filepath)) {
633
+ try {
634
+ await node_fs_1.promises.rm(filepath, { recursive: true, force: true });
635
+ utils_js_1.logger.info('删除文件:', (0, console_log_colors_1.gray)(filepath));
636
+ }
637
+ catch (error) {
638
+ const errorMsg = `删除文件失败: ${filepath}, 错误: ${error.message}`;
639
+ utils_js_1.logger.error(errorMsg);
640
+ errors.push(errorMsg);
641
+ // 如果直接删除失败,可能是文件被占用
642
+ }
643
+ }
644
+ }
645
+ else {
646
+ // 如果 localVideo 不存在,尝试使用 dlOptions 构建路径(格式化后的参数更准确)
647
+ const saveDir = item.dlOptions?.saveDir || item.options?.saveDir;
648
+ const filename = item.dlOptions?.filename || item.options?.filename;
649
+ if (saveDir && filename) {
650
+ // 尝试多种可能的扩展名
651
+ for (const ext of ['', '.ts', '.mp4']) {
652
+ const filepath = (0, node_path_1.resolve)(saveDir, filename + ext);
653
+ if (await (0, utils_js_1.checkFileExists)(filepath)) {
654
+ try {
655
+ await node_fs_1.promises.rm(filepath, { recursive: true, force: true });
656
+ utils_js_1.logger.info('删除文件:', (0, console_log_colors_1.gray)(filepath));
657
+ break; // 找到并删除后退出循环
658
+ }
659
+ catch (error) {
660
+ const errorMsg = `删除文件失败: ${filepath}, 错误: ${error.message}`;
661
+ utils_js_1.logger.error(errorMsg);
662
+ errors.push(errorMsg);
663
+ }
664
+ }
665
+ }
666
+ }
667
+ else {
668
+ const errorMsg = `无法确定文件路径: saveDir=${saveDir}, filename=${filename}`;
669
+ utils_js_1.logger.warn(errorMsg, (0, console_log_colors_1.gray)(url));
670
+ errors.push(errorMsg);
671
+ }
604
672
  }
605
673
  }
674
+ catch (error) {
675
+ const errorMsg = `删除视频文件时发生错误: ${error.message}`;
676
+ utils_js_1.logger.error(errorMsg, (0, console_log_colors_1.gray)(url));
677
+ errors.push(errorMsg);
678
+ }
606
679
  }
607
680
  }
608
681
  }
@@ -612,7 +685,10 @@ class DLServer {
612
685
  this.startNextPending();
613
686
  }
614
687
  const lang = this.getLangFromRequest(req);
615
- res.json({ message: (0, i18n_js_1.t)('api.success.deleted', lang, { count: list.length }), code: 0, count: list.length });
688
+ const message = errors.length
689
+ ? `${(0, i18n_js_1.t)('api.success.deleted', lang, { count: list.length })},但有 ${errors.length} 个错误: ${errors.join('; ')}`
690
+ : (0, i18n_js_1.t)('api.success.deleted', lang, { count: list.length });
691
+ res.json({ message, code: errors.length > 0 ? 1 : 0, count: list.length, errors: errors.length > 0 ? errors : undefined });
616
692
  });
617
693
  // API to rename download file
618
694
  app.post('/api/rename', async (req, res) => {
@@ -144,6 +144,12 @@ export interface M3u8DLOptions {
144
144
  ffmpegPath?: string;
145
145
  /** 语言。可选值:zh-CN, en */
146
146
  lang?: 'zh-CN' | 'en';
147
+ /** 代理模式。可选值:custom, system, disabled */
148
+ proxyMode?: 'custom' | 'system' | 'disabled';
149
+ /** 代理地址。如果 proxyMode 为 'custom',则必须指定 */
150
+ proxyUrl?: string;
151
+ /** 不使用代理的域名。多个域名用逗号分隔 */
152
+ noProxy?: string;
147
153
  }
148
154
  export interface M3u8DLResult extends Partial<DownloadResult> {
149
155
  /** 下载进度统计 */
@@ -4,8 +4,8 @@ export declare class VideoParser {
4
4
  /**
5
5
  * 解析视频 URL
6
6
  */
7
- parse(url: string, headers?: Record<string, string>): Promise<ApiResponse<VideoInfo>>;
8
- download(url: string, options: M3u8DLOptions): Promise<M3u8DLResult>;
7
+ static parse(url: string, headers?: Record<string, string>): Promise<ApiResponse<VideoInfo>>;
8
+ static download(url: string, options: M3u8DLOptions): Promise<M3u8DLResult>;
9
9
  /**
10
10
  * 根据 URL 获取平台标识
11
11
  */
@@ -16,5 +16,5 @@ export declare class VideoParser {
16
16
  /**
17
17
  * 获取所有支持的平台列表
18
18
  */
19
- getSupportedPlatforms(): string[];
19
+ static getSupportedPlatforms(): string[];
20
20
  }
@@ -31,15 +31,15 @@ class VideoParser {
31
31
  /**
32
32
  * 解析视频 URL
33
33
  */
34
- async parse(url, headers = {}) {
34
+ static async parse(url, headers = {}) {
35
35
  const info = VideoParser.getPlatform(url);
36
36
  if (!info)
37
37
  return { code: 201, message: '不支持的视频平台' };
38
38
  const parserClass = VideoParser.platforms[info.platform].class;
39
39
  return await parserClass.parse(info.url, headers);
40
40
  }
41
- async download(url, options) {
42
- const info = await this.parse(url);
41
+ static async download(url, options) {
42
+ const info = await VideoParser.parse(url);
43
43
  utils_1.logger.debug('解析视频信息', info);
44
44
  if (info.code || !info.data?.url)
45
45
  return { errmsg: info.message || '解析视频信息失败', options };
@@ -54,7 +54,7 @@ class VideoParser {
54
54
  referer: info.data.referer || info.data.url,
55
55
  ...(0, utils_1.formatHeaders)(options.headers),
56
56
  };
57
- return (0, file_download_1.fileDownload)(url, options);
57
+ return (0, file_download_1.fileDownload)(info.data.url, options);
58
58
  }
59
59
  /**
60
60
  * 根据 URL 获取平台标识
@@ -79,7 +79,7 @@ class VideoParser {
79
79
  /**
80
80
  * 获取所有支持的平台列表
81
81
  */
82
- getSupportedPlatforms() {
82
+ static getSupportedPlatforms() {
83
83
  return Object.keys(VideoParser.platforms);
84
84
  }
85
85
  }