@lzwme/m3u8-dl 1.4.3 → 1.6.0-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.
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ /**
3
+ * Shared i18n utility for backend (CLI, SDK, Server)
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.LANG_CODES = void 0;
7
+ exports.detectLanguage = detectLanguage;
8
+ exports.setLanguage = setLanguage;
9
+ exports.getLanguage = getLanguage;
10
+ exports.getLang = getLang;
11
+ exports.t = t;
12
+ exports.LANG_CODES = new Set(['zh', 'en']);
13
+ let globalLang = null;
14
+ /**
15
+ * Detect language from OS or environment
16
+ */
17
+ function detectLanguage() {
18
+ // Check environment variable first
19
+ const envLang = process.env.M3U8DL_LANG || process.env.LANG;
20
+ if (envLang) {
21
+ const langCode = envLang.toLowerCase().split('-')[0].split('_')[0];
22
+ if (exports.LANG_CODES.has(langCode)) {
23
+ return langCode;
24
+ }
25
+ }
26
+ // Try to detect from OS locale
27
+ try {
28
+ const osLocale = Intl.DateTimeFormat().resolvedOptions().locale;
29
+ const langCode = osLocale.toLowerCase().split('-')[0];
30
+ if (exports.LANG_CODES.has(langCode)) {
31
+ return langCode;
32
+ }
33
+ }
34
+ catch {
35
+ // Ignore errors
36
+ }
37
+ // Fallback to English
38
+ return 'en';
39
+ }
40
+ /**
41
+ * Set global language context
42
+ */
43
+ function setLanguage(lang) {
44
+ globalLang = lang;
45
+ }
46
+ /**
47
+ * Get global language context
48
+ */
49
+ function getLanguage() {
50
+ return globalLang;
51
+ }
52
+ /**
53
+ * Get language from various sources
54
+ */
55
+ function getLang(lang) {
56
+ if (lang && exports.LANG_CODES.has(lang)) {
57
+ return lang;
58
+ }
59
+ if (globalLang) {
60
+ return globalLang;
61
+ }
62
+ return detectLanguage();
63
+ }
64
+ /**
65
+ * Translation function
66
+ */
67
+ function t(key, lang, params) {
68
+ const targetLang = getLang(lang);
69
+ const translations = require(`../i18n/locales/${targetLang}.js`);
70
+ // Navigate through nested keys (e.g., 'cli.command.download.description')
71
+ const keys = key.split('.');
72
+ let value = translations.default || translations;
73
+ for (const k of keys) {
74
+ if (value && typeof value === 'object' && k in value) {
75
+ value = value[k];
76
+ }
77
+ else {
78
+ // Key not found, return the key itself
79
+ return key;
80
+ }
81
+ }
82
+ // If value is a string, replace params if provided
83
+ if (typeof value === 'string' && params) {
84
+ return value.replace(/\{(\w+)\}/g, (match, paramKey) => {
85
+ return params[paramKey] !== undefined ? String(params[paramKey]) : match;
86
+ });
87
+ }
88
+ return typeof value === 'string' ? value : key;
89
+ }
@@ -7,7 +7,8 @@ const fe_utils_1 = require("@lzwme/fe-utils");
7
7
  const console_log_colors_1 = require("console-log-colors");
8
8
  const utils_1 = require("./utils");
9
9
  async function m3u8Convert(options, data) {
10
- let ffmpegSupport = (0, utils_1.isSupportFfmpeg)();
10
+ const ffmpegBin = options.ffmpegPath || 'ffmpeg';
11
+ let ffmpegSupport = (0, utils_1.isSupportFfmpeg)(ffmpegBin);
11
12
  let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename);
12
13
  if (!ffmpegSupport)
13
14
  filepath = filepath.replace(/\.mp4$/, '.ts');
@@ -28,13 +29,13 @@ async function m3u8Convert(options, data) {
28
29
  }
29
30
  }
30
31
  // ffmpeg -i nz.ts -c copy -map 0:v -map 0:a -bsf:a aac_adtstoasc nz.mp4
31
- // const cmd = `ffmpeg -async 1 -y -f concat -safe 0 -i "${ffconcatFile}" -acodec copy -vcodec copy -bsf:a aac_adtstoasc ${headersString} "${filepath}"`;
32
- const cmd = `ffmpeg -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}"`;
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}"`;
33
34
  utils_1.logger.debug('[convert to mp4]cmd:', (0, console_log_colors_1.cyan)(cmd));
34
35
  const r = (0, fe_utils_1.execSync)(cmd);
35
36
  ffmpegSupport = !r.error;
36
37
  if (r.error)
37
- utils_1.logger.error('Conversion to mp4 failed. Please confirm that `ffmpeg` is installed!', r.stderr);
38
+ utils_1.logger.error(`Conversion to mp4 failed. Please confirm that \`${ffmpegBin}\` is installed and available!`, r.stderr);
38
39
  else
39
40
  (0, node_fs_1.unlinkSync)(ffconcatFile);
40
41
  }
@@ -9,6 +9,7 @@ const node_path_1 = require("node:path");
9
9
  const fe_utils_1 = require("@lzwme/fe-utils");
10
10
  const helper_1 = require("@lzwme/fe-utils/cjs/common/helper");
11
11
  const console_log_colors_1 = require("console-log-colors");
12
+ const i18n_js_1 = require("./i18n.js");
12
13
  const format_options_js_1 = require("./format-options.js");
13
14
  const local_play_js_1 = require("./local-play.js");
14
15
  const m3u8_convert_js_1 = require("./m3u8-convert.js");
@@ -87,8 +88,9 @@ const cache = {
87
88
  const tsDlFile = (0, node_path_1.resolve)(__dirname, './ts-download.js');
88
89
  exports.workPollPublic = new worker_pool_js_1.WorkerPool(tsDlFile);
89
90
  async function m3u8InfoParse(u, o = {}) {
91
+ const ffmpegBin = o.ffmpegPath || 'ffmpeg';
92
+ const ext = (0, utils_js_1.isSupportFfmpeg)(ffmpegBin) ? '.mp4' : '.ts';
90
93
  const { url, options, urlMd5 } = (0, format_options_js_1.formatOptions)(u, o);
91
- const ext = (0, utils_js_1.isSupportFfmpeg)() ? '.mp4' : '.ts';
92
94
  /** 最终合并转换后的文件路径 */
93
95
  let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename);
94
96
  if (!filepath.endsWith(ext))
@@ -100,17 +102,32 @@ async function m3u8InfoParse(u, o = {}) {
100
102
  }
101
103
  if (!options.force && (0, node_fs_1.existsSync)(filepath))
102
104
  return result;
105
+ const lang = (0, i18n_js_1.getLang)(o.lang);
103
106
  const m3u8Info = await (0, parseM3u8_js_1.parseM3U8)(url, (0, node_path_1.resolve)(options.cacheDir, urlMd5), options.headers).catch(e => {
104
- utils_js_1.logger.error('[parseM3U8][failed]', e.message);
107
+ utils_js_1.logger.error((0, i18n_js_1.t)('download.status.parseFailed', lang), e.message);
105
108
  console.log(e);
106
109
  });
107
110
  if (m3u8Info && m3u8Info?.tsCount > 0) {
108
111
  result.m3u8Info = m3u8Info;
109
112
  if (options.ignoreSegments) {
113
+ const totalDuration = m3u8Info.duration;
110
114
  const timeSegments = options.ignoreSegments
111
115
  .split(',')
112
- .map(d => d.split(/[- ]+/).map(d => +d.trim()))
113
- .filter(d => d[0] && d[1] && d[0] !== d[1]);
116
+ .map(d => {
117
+ const trimmed = d.trim();
118
+ // 支持 END-60 格式,表示末尾N秒
119
+ const parts = trimmed.split(/[- ]+/).map(d => d.trim());
120
+ if (parts.length === 2 && parts[0].toUpperCase() === 'END') {
121
+ const seconds = +parts[1];
122
+ if (!Number.isNaN(seconds) && seconds > 0) {
123
+ const start = Math.max(0, totalDuration - seconds);
124
+ return [start, totalDuration];
125
+ }
126
+ }
127
+ // 原有的 start-end 格式
128
+ return parts.map(d => +d);
129
+ })
130
+ .filter(d => d[0] !== undefined && d[1] !== undefined && !Number.isNaN(d[0]) && !Number.isNaN(d[1]) && d[0] !== d[1]);
114
131
  if (timeSegments.length) {
115
132
  const total = m3u8Info.data.length;
116
133
  m3u8Info.data = m3u8Info.data.filter(item => {
@@ -127,7 +144,7 @@ async function m3u8InfoParse(u, o = {}) {
127
144
  const ignoredCount = total - m3u8Info.data.length;
128
145
  if (ignoredCount) {
129
146
  m3u8Info.tsCount = m3u8Info.data.length;
130
- utils_js_1.logger.info(`[parseM3U8][ignoreSegments] ignored ${(0, console_log_colors_1.cyanBright)(ignoredCount)} segments`);
147
+ utils_js_1.logger.info((0, i18n_js_1.t)('download.status.segmentsIgnored', lang, { count: (0, console_log_colors_1.cyanBright)(ignoredCount) }));
131
148
  m3u8Info.duration = +Number(m3u8Info.duration).toFixed(2);
132
149
  }
133
150
  }
@@ -184,11 +201,12 @@ async function m3u8Download(url, options = {}) {
184
201
  });
185
202
  }
186
203
  // 原有的下载逻辑
187
- utils_js_1.logger.info('Starting download for', (0, console_log_colors_1.cyanBright)(url));
204
+ const lang = (0, i18n_js_1.getLang)(options.lang);
205
+ utils_js_1.logger.info((0, i18n_js_1.t)('download.status.starting', lang), (0, console_log_colors_1.cyanBright)(url));
188
206
  const result = await m3u8InfoParse(url, options);
189
207
  options = result.options;
190
208
  if (!options.force && (0, node_fs_1.existsSync)(result.filepath) && !result.m3u8Info) {
191
- utils_js_1.logger.info('file already exist:', result.filepath);
209
+ utils_js_1.logger.info((0, i18n_js_1.t)('download.status.fileExists', lang), result.filepath);
192
210
  result.isExist = true;
193
211
  return result;
194
212
  }
@@ -226,14 +244,14 @@ async function m3u8Download(url, options = {}) {
226
244
  if (!res || err) {
227
245
  if (err) {
228
246
  console.log('\n');
229
- utils_js_1.logger.error('[TS-DL][error]', info.index, err, res || '');
247
+ utils_js_1.logger.error((0, i18n_js_1.t)('download.status.tsDownloadError', lang), info.index, err, res || '');
230
248
  }
231
249
  if (typeof info.success !== 'number')
232
250
  info.success = 0;
233
251
  else
234
252
  info.success--;
235
253
  if (info.success >= -3) {
236
- utils_js_1.logger.warn(`[retry][times: ${info.success}]`, info.index, info.uri);
254
+ utils_js_1.logger.warn((0, i18n_js_1.t)('download.status.retryTimes', lang, { times: info.success }), info.index, info.uri);
237
255
  setTimeout(() => runTask([info]), 1000);
238
256
  return;
239
257
  }
@@ -288,7 +306,7 @@ async function m3u8Download(url, options = {}) {
288
306
  }
289
307
  };
290
308
  if (options.showProgress) {
291
- console.info(`\nTotal segments: ${(0, console_log_colors_1.cyan)(m3u8Info.tsCount)}, duration: ${(0, console_log_colors_1.green)(`${m3u8Info.duration}sec`)}.`, `Parallel jobs: ${(0, console_log_colors_1.magenta)(options.threadNum)}`);
309
+ console.info(`\n${(0, i18n_js_1.t)('download.status.totalSegments', lang, { count: (0, console_log_colors_1.cyan)(m3u8Info.tsCount), duration: (0, console_log_colors_1.green)(`${m3u8Info.duration}sec`) })}.`, (0, i18n_js_1.t)('download.status.parallelJobs', lang, { count: (0, console_log_colors_1.magenta)(options.threadNum) }));
292
310
  }
293
311
  result.stats = stats;
294
312
  (0, local_play_js_1.toLocalM3u8)(m3u8Info.data);
@@ -298,16 +316,24 @@ async function m3u8Download(url, options = {}) {
298
316
  await barrier.wait();
299
317
  workPoll.close();
300
318
  if (stats.tsFailed > 0) {
301
- utils_js_1.logger.warn('Download Failed! Please retry!', stats.tsFailed);
319
+ stats.errmsg = (0, i18n_js_1.t)('download.status.segmentsFailed', lang, { count: stats.tsFailed });
320
+ result.errmsg = stats.errmsg;
321
+ utils_js_1.logger.warn((0, i18n_js_1.t)('download.status.downloadFailedRetry', lang), stats.tsFailed);
302
322
  }
303
323
  else if (options.convert !== false) {
324
+ stats.errmsg = (0, i18n_js_1.t)('download.status.mergingVideo', lang);
325
+ if (options.onProgress)
326
+ options.onProgress(stats.tsCount, m3u8Info.tsCount, null, stats);
304
327
  result.filepath = await (0, m3u8_convert_js_1.m3u8Convert)(options, m3u8Info.data);
328
+ stats.errmsg = result.filepath ? '' : (0, i18n_js_1.t)('download.status.mergeFailed', lang);
305
329
  if (result.filepath && (0, node_fs_1.existsSync)(result.filepath)) {
306
330
  stats.size = (0, node_fs_1.statSync)(result.filepath).size;
307
331
  if (options.delCache)
308
332
  (0, fe_utils_1.rmrfAsync)((0, node_path_1.dirname)(m3u8Info.data[0].tsOut));
309
333
  }
310
334
  }
335
+ if (options.onProgress)
336
+ options.onProgress(stats.tsCount, m3u8Info.tsCount, null, stats);
311
337
  }
312
338
  utils_js_1.logger.debug('Done!', url, result.m3u8Info);
313
339
  return result;
@@ -9,7 +9,7 @@ export declare const getRetry: <T = string>(url: string, headers?: OutgoingHttpH
9
9
  response: import("http").IncomingMessage;
10
10
  }>;
11
11
  export declare const logger: NLogger;
12
- export declare function isSupportFfmpeg(): boolean;
12
+ export declare function isSupportFfmpeg(ffmpegBin: string): boolean;
13
13
  export declare function findFiles(apidir?: string, validate?: (filepath: string, stat: Stats) => boolean): string[];
14
14
  /** 获取重定向后的 URL */
15
15
  export declare function getLocation(url: string, method?: string): Promise<string>;
package/cjs/lib/utils.js CHANGED
@@ -23,11 +23,11 @@ const getRetry = (url, headers, retries = 3) => (0, fe_utils_1.retry)(() => expo
23
23
  });
24
24
  exports.getRetry = getRetry;
25
25
  exports.logger = fe_utils_1.NLogger.getLogger('[M3U8-DL]', { color: fe_utils_1.color });
26
- let _isSupportFfmpeg = null;
27
- function isSupportFfmpeg() {
28
- if (null == _isSupportFfmpeg)
29
- _isSupportFfmpeg = (0, fe_utils_1.execSync)('ffmpeg -version').stderr === '';
30
- return _isSupportFfmpeg;
26
+ const ffmpegTestCache = {};
27
+ function isSupportFfmpeg(ffmpegBin) {
28
+ if (!(ffmpegBin in ffmpegTestCache))
29
+ ffmpegTestCache[ffmpegBin] = (0, fe_utils_1.execSync)(`${ffmpegBin} -version`).stderr === '';
30
+ return ffmpegTestCache[ffmpegBin];
31
31
  }
32
32
  function findFiles(apidir, validate) {
33
33
  const files = [];
@@ -46,6 +46,7 @@ export declare class DLServer {
46
46
  private createApp;
47
47
  private startDownload;
48
48
  startNextPending(): void;
49
+ private getLangFromRequest;
49
50
  private wsSend;
50
51
  private initRouters;
51
52
  }
@@ -35,14 +35,15 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.DLServer = void 0;
37
37
  const node_fs_1 = require("node:fs");
38
- const node_path_1 = require("node:path");
39
38
  const node_os_1 = require("node:os");
39
+ const node_path_1 = require("node:path");
40
40
  const fe_utils_1 = require("@lzwme/fe-utils");
41
41
  const console_log_colors_1 = require("console-log-colors");
42
42
  const file_download_js_1 = require("../lib/file-download.js");
43
43
  const format_options_js_1 = require("../lib/format-options.js");
44
- const m3u8_download_js_1 = require("../lib/m3u8-download.js");
45
44
  const getM3u8Urls_js_1 = require("../lib/getM3u8Urls.js");
45
+ const i18n_js_1 = require("../lib/i18n.js");
46
+ const m3u8_download_js_1 = require("../lib/m3u8-download.js");
46
47
  const utils_js_1 = require("../lib/utils.js");
47
48
  const index_js_1 = require("../video-parser/index.js");
48
49
  const rootDir = (0, node_path_1.resolve)(__dirname, '../..');
@@ -76,6 +77,7 @@ class DLServer {
76
77
  debug: process.env.DS_DEBUG === '1',
77
78
  saveDir: process.env.DS_SAVE_DIR || './downloads',
78
79
  threadNum: 4,
80
+ ffmpegPath: process.env.DS_FFMPEG_PATH || undefined,
79
81
  },
80
82
  };
81
83
  /** 下载任务缓存 */
@@ -172,6 +174,18 @@ class DLServer {
172
174
  saveConfig(config, configPath) {
173
175
  if (!configPath)
174
176
  configPath = this.options.configPath;
177
+ // 验证 ffmpegPath 是否存在
178
+ if (config.ffmpegPath?.trim()) {
179
+ const ffmpegPath = config.ffmpegPath.trim();
180
+ if (!(0, node_fs_1.existsSync)(ffmpegPath)) {
181
+ throw new Error(`ffmpeg 路径不存在: ${ffmpegPath}`);
182
+ }
183
+ // 检查是否为文件(不是目录)
184
+ const stats = (0, node_fs_1.statSync)(ffmpegPath);
185
+ if (!stats.isFile()) {
186
+ throw new Error(`ffmpeg 路径不是文件: ${ffmpegPath}`);
187
+ }
188
+ }
175
189
  for (const [key, value] of Object.entries(config)) {
176
190
  // @ts-expect-error 忽略类型错误
177
191
  if (key in this.cfg.webOptions)
@@ -192,16 +206,28 @@ class DLServer {
192
206
  this.app = app;
193
207
  this.wss = wss;
194
208
  app.use((req, res, next) => {
195
- if (['/', '/index.html'].includes(req.path)) {
209
+ // 处理 SPA 路由:根路径和 /page/* 路径都返回 index.html
210
+ const isIndexPage = ['/', '/index.html'].includes(req.path) || req.path.startsWith('/page/');
211
+ const isPlayPage = req.path.startsWith('/play.html');
212
+ if (isIndexPage || isPlayPage) {
196
213
  const version = this.serverInfo.version;
197
- let indexHtml = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(rootDir, 'client/index.html'), 'utf-8').replaceAll('{{version}}', 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);
198
215
  if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(rootDir, 'client/local/cdn'))) {
199
- indexHtml = indexHtml
200
- .replaceAll('https://s4.zstatic.net/ajax/libs', 'local/cdn')
201
- .replaceAll(/integrity="[^"]+"\n?/g, '')
202
- .replace('https://cdn.tailwindcss.com/3.4.16', 'local/cdn/tailwindcss/3.4.16/tailwindcss.min.js');
216
+ // 提取所有 zstatic.net 的 js 和 css 资源地址,若子路径存在于 local/cdn 目录下则替换为本地路径
217
+ const zstaticRegex = /https:\/\/s4\.zstatic\.net\/ajax\/libs\/[^\s"'`<>]+\.(js|css)/g;
218
+ const zstaticMatches = htmlContent.match(zstaticRegex);
219
+ if (zstaticMatches) {
220
+ for (const match of zstaticMatches) {
221
+ const relativePath = match.split('libs/')[1];
222
+ 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}`);
225
+ }
226
+ }
227
+ htmlContent = htmlContent.replaceAll(/integrity="[^"]+"\n?/g, '');
228
+ }
203
229
  }
204
- res.setHeader('content-type', 'text/html').send(indexHtml);
230
+ res.setHeader('content-type', 'text/html').send(htmlContent);
205
231
  }
206
232
  else {
207
233
  next();
@@ -221,9 +247,10 @@ class DLServer {
221
247
  if (this.options.token && req.headers.authorization !== this.options.token) {
222
248
  const ignorePaths = ['/healthcheck', '/localplay'];
223
249
  if (!ignorePaths.some(d => req.url.includes(d))) {
250
+ const lang = this.getLangFromRequest(req);
224
251
  const clientIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
225
252
  utils_js_1.logger.warn('Unauthorized access:', clientIp, req.url, req.headers.authorization);
226
- res.status(401).json({ message: '未授权,禁止访问', code: 401 });
253
+ res.status(401).json({ message: (0, i18n_js_1.t)('api.error.unauthorized', lang), code: 1008 });
227
254
  return;
228
255
  }
229
256
  }
@@ -338,6 +365,28 @@ class DLServer {
338
365
  this.wsSend('progress', nextItem[0]);
339
366
  }
340
367
  }
368
+ getLangFromRequest(req) {
369
+ // Try to get lang from query parameter
370
+ const queryLang = req.query?.lang;
371
+ if (queryLang && i18n_js_1.LANG_CODES.has(queryLang)) {
372
+ return queryLang;
373
+ }
374
+ // Try to get lang from Accept-Language header
375
+ const acceptLanguage = req.headers['accept-language'];
376
+ if (acceptLanguage) {
377
+ const langCode = acceptLanguage.toLowerCase().split(',')[0].split('-')[0].trim();
378
+ if (i18n_js_1.LANG_CODES.has(langCode)) {
379
+ return langCode;
380
+ }
381
+ }
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
+ // Fallback to default
388
+ return (0, i18n_js_1.getLang)();
389
+ }
341
390
  wsSend(type = 'progress', data) {
342
391
  if (type === 'tasks' && !data) {
343
392
  data = Object.fromEntries(this.dlCacheClone());
@@ -361,29 +410,40 @@ class DLServer {
361
410
  res.json({ message: 'ok', code: 0 });
362
411
  });
363
412
  app.post('/api/config', (req, res) => {
364
- const config = req.body;
365
- this.saveConfig(config);
366
- res.json({ message: 'Config updated successfully', code: 0 });
413
+ const lang = this.getLangFromRequest(req);
414
+ try {
415
+ const config = req.body;
416
+ this.saveConfig(config);
417
+ res.json({ message: (0, i18n_js_1.t)('api.success.configUpdated', lang), code: 0 });
418
+ }
419
+ catch (error) {
420
+ const errorMessage = error instanceof Error ? error.message : (0, i18n_js_1.t)('api.error.configSaveFailed', lang);
421
+ utils_js_1.logger.error('[saveConfig]', errorMessage);
422
+ res.status(400).json({ message: errorMessage, code: 1 });
423
+ }
367
424
  });
368
425
  app.get('/api/config', (_req, res) => {
369
- res.json({ ...this.cfg.dlOptions, ...this.cfg.webOptions });
426
+ res.json({ code: 0, data: { ...this.cfg.dlOptions, ...this.cfg.webOptions } });
370
427
  });
371
428
  // API to get all download progress
372
429
  app.get('/api/tasks', (_req, res) => {
373
- res.json(Object.fromEntries(this.dlCacheClone()));
430
+ res.json({ code: 0, data: Object.fromEntries(this.dlCacheClone()) });
374
431
  });
375
432
  // API to get queue status
376
433
  app.get('/api/queue/status', (_req, res) => {
377
434
  const pendingTasks = Array.from(this.dlCache.entries()).filter(([_, item]) => item.status === 'pending');
378
435
  const activeTasks = Array.from(this.dlCache.entries()).filter(([_, item]) => item.status === 'resume');
379
436
  res.json({
380
- queueLength: pendingTasks.length,
381
- activeDownloads: activeTasks.map(([url]) => url),
382
- maxConcurrent: this.cfg.webOptions.maxDownloads,
437
+ code: 0,
438
+ data: {
439
+ queueLength: pendingTasks.length,
440
+ activeDownloads: activeTasks.map(([url]) => url),
441
+ maxConcurrent: this.cfg.webOptions.maxDownloads,
442
+ },
383
443
  });
384
444
  });
385
445
  // API to clear queue
386
- app.post('/api/queue/clear', (_req, res) => {
446
+ app.post('/api/queue/clear', (req, res) => {
387
447
  let count = 0;
388
448
  for (const [url, item] of this.dlCache.entries()) {
389
449
  if (item.status === 'pending') {
@@ -393,23 +453,25 @@ class DLServer {
393
453
  }
394
454
  if (count)
395
455
  this.wsSend('tasks');
396
- res.json({ message: `已清空 ${count} 个等待中的下载任务`, code: 0 });
456
+ const lang = this.getLangFromRequest(req);
457
+ res.json({ message: (0, i18n_js_1.t)('api.success.queueCleared', lang, { count }), code: 0 });
397
458
  });
398
459
  // API to update task priority
399
- app.post('/api/priority', (req, res) => {
400
- const { url, priority } = req.body;
401
- const item = this.dlCache.get(url);
402
- if (!item) {
403
- res.json({ message: '任务不存在', code: 1 });
404
- return;
405
- }
406
- item.options.priority = priority;
407
- this.saveCache();
408
- res.json({ message: '已更新任务优先级', code: 0 });
409
- });
460
+ // app.post('/api/priority', (req, res) => {
461
+ // const { url, priority } = req.body;
462
+ // const item = this.dlCache.get(url);
463
+ // if (!item) {
464
+ // res.json({ message: '任务不存在', code: 1 });
465
+ // return;
466
+ // }
467
+ // item.options.priority = priority;
468
+ // this.saveCache();
469
+ // res.json({ message: '已更新任务优先级', code: 0 });
470
+ // });
410
471
  // API to start m3u8 download
411
472
  app.post('/api/download', (req, res) => {
412
473
  const { url, options = {}, list = [] } = req.body;
474
+ const lang = this.getLangFromRequest(req);
413
475
  try {
414
476
  if (list.length) {
415
477
  for (const item of list) {
@@ -420,11 +482,11 @@ class DLServer {
420
482
  }
421
483
  else if (url)
422
484
  this.startDownload(url, options);
423
- res.json({ message: `Download started: ${list.length || 1}`, code: 0 });
485
+ res.json({ message: (0, i18n_js_1.t)('api.success.downloadStarted', lang, { count: list.length || 1 }), code: 0 });
424
486
  this.wsSend('tasks');
425
487
  }
426
488
  catch (error) {
427
- res.status(500).json({ error: `Download failed: ${error.message}` });
489
+ res.status(500).json({ error: `${(0, i18n_js_1.t)('api.error.downloadFailed', lang)}: ${error.message || ''}` });
428
490
  }
429
491
  });
430
492
  // API to pause download
@@ -445,7 +507,8 @@ class DLServer {
445
507
  this.wsSend('progress', list);
446
508
  this.startNextPending();
447
509
  }
448
- res.json({ message: `已暂停 ${list.length} 个下载任务`, code: 0, count: list.length });
510
+ const lang = this.getLangFromRequest(req);
511
+ res.json({ message: (0, i18n_js_1.t)('api.success.paused', lang, { count: list.length }), code: 0, count: list.length });
449
512
  });
450
513
  // API to resume download
451
514
  app.post('/api/resume', (req, res) => {
@@ -464,7 +527,12 @@ class DLServer {
464
527
  }
465
528
  if (list.length)
466
529
  this.wsSend('progress', list);
467
- res.json({ message: list.length ? `已恢复 ${list.length} 个下载任务` : '没有找到可恢复的下载任务', code: 0, count: list.length });
530
+ const lang = this.getLangFromRequest(req);
531
+ res.json({
532
+ message: list.length ? (0, i18n_js_1.t)('api.success.resumed', lang, { count: list.length }) : (0, i18n_js_1.t)('api.success.noResumableTasks', lang),
533
+ code: 0,
534
+ count: list.length,
535
+ });
468
536
  });
469
537
  // API to delete download
470
538
  app.post('/api/delete', (req, res) => {
@@ -500,7 +568,8 @@ class DLServer {
500
568
  this.saveCache();
501
569
  this.startNextPending();
502
570
  }
503
- res.json({ message: `已删除 ${list.length} 个下载任务`, code: 0, count: list.length });
571
+ const lang = this.getLangFromRequest(req);
572
+ res.json({ message: (0, i18n_js_1.t)('api.success.deleted', lang, { count: list.length }), code: 0, count: list.length });
504
573
  });
505
574
  app.get(/^\/localplay\/(.*)$/, (req, res) => {
506
575
  let filepath = decodeURIComponent(req.params[0]);
@@ -526,7 +595,8 @@ class DLServer {
526
595
  const isAllow = !this.options.limitFileAccess || allowedDirs.some(d => filepath.startsWith((0, node_path_1.resolve)(d)));
527
596
  if (!isAllow) {
528
597
  utils_js_1.logger.error('[Localplay] Access denied:', filepath);
529
- res.send({ message: 'Access denied', code: 403 });
598
+ const lang = this.getLangFromRequest(req);
599
+ res.send({ message: (0, i18n_js_1.t)('api.error.accessDenied', lang), code: 403 });
530
600
  return;
531
601
  }
532
602
  }
@@ -553,12 +623,14 @@ class DLServer {
553
623
  }
554
624
  }
555
625
  utils_js_1.logger.error('[Localplay]file not found:', (0, console_log_colors_1.red)(filepath));
556
- res.status(404).send({ message: 'Not Found', code: 404 });
626
+ const lang = this.getLangFromRequest(req);
627
+ res.status(404).send({ message: (0, i18n_js_1.t)('api.error.notFound', lang), code: 404 });
557
628
  });
558
629
  app.post('/api/getM3u8Urls', (req, res) => {
559
630
  const { url, headers, subUrlRegex } = req.body;
631
+ const lang = this.getLangFromRequest(req);
560
632
  if (!url) {
561
- res.json({ code: 1001, message: '无效的 url 参数' });
633
+ res.json({ code: 1001, message: (0, i18n_js_1.t)('api.error.invalidUrl', lang) });
562
634
  }
563
635
  else {
564
636
  (0, getM3u8Urls_js_1.getM3u8Urls)({ url, headers, subUrlRegex })
@@ -1,4 +1,4 @@
1
1
  export * from './common';
2
2
  export * from './m3u8';
3
- export * from './video-search';
4
3
  export * from './video-parser';
4
+ export * from './video-search';
@@ -16,5 +16,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./common"), exports);
18
18
  __exportStar(require("./m3u8"), exports);
19
- __exportStar(require("./video-search"), exports);
20
19
  __exportStar(require("./video-parser"), exports);
20
+ __exportStar(require("./video-search"), exports);
@@ -140,6 +140,10 @@ export interface M3u8DLOptions {
140
140
  * - 'parser':下载 VideoParser 支持解析的平台视频文件
141
141
  */
142
142
  type?: 'm3u8' | 'file' | 'parser';
143
+ /** ffmpeg 可执行文件路径。如果未指定,则尝试使用系统 PATH 中的 'ffmpeg' */
144
+ ffmpegPath?: string;
145
+ /** 语言。可选值:zh, en */
146
+ lang?: 'zh' | 'en';
143
147
  }
144
148
  export interface M3u8DLResult extends Partial<DownloadResult> {
145
149
  /** 下载进度统计 */