@lzwme/m3u8-dl 1.5.0 → 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
+ }
@@ -1,18 +1,13 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.m3u8Convert = m3u8Convert;
7
4
  const node_fs_1 = require("node:fs");
8
5
  const node_path_1 = require("node:path");
9
6
  const fe_utils_1 = require("@lzwme/fe-utils");
10
7
  const console_log_colors_1 = require("console-log-colors");
11
- const ffmpeg_static_1 = __importDefault(require("ffmpeg-static"));
12
8
  const utils_1 = require("./utils");
13
9
  async function m3u8Convert(options, data) {
14
- const useGlobal = options.useGlobalFfmpeg || false;
15
- const ffmpegBin = useGlobal ? 'ffmpeg' : ffmpeg_static_1.default;
10
+ const ffmpegBin = options.ffmpegPath || 'ffmpeg';
16
11
  let ffmpegSupport = (0, utils_1.isSupportFfmpeg)(ffmpegBin);
17
12
  let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename);
18
13
  if (!ffmpegSupport)
@@ -40,7 +35,7 @@ async function m3u8Convert(options, data) {
40
35
  const r = (0, fe_utils_1.execSync)(cmd);
41
36
  ffmpegSupport = !r.error;
42
37
  if (r.error)
43
- utils_1.logger.error(`Conversion to mp4 failed. Please confirm that \`${useGlobal ? 'ffmpeg' : 'ffmpeg-static'}\` is ${useGlobal ? 'installed' : 'available'}!`, r.stderr);
38
+ utils_1.logger.error(`Conversion to mp4 failed. Please confirm that \`${ffmpegBin}\` is installed and available!`, r.stderr);
44
39
  else
45
40
  (0, node_fs_1.unlinkSync)(ffconcatFile);
46
41
  }
@@ -1,7 +1,4 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.workPollPublic = exports.downloadQueue = exports.DownloadQueue = void 0;
7
4
  exports.preDownLoad = preDownLoad;
@@ -12,7 +9,7 @@ const node_path_1 = require("node:path");
12
9
  const fe_utils_1 = require("@lzwme/fe-utils");
13
10
  const helper_1 = require("@lzwme/fe-utils/cjs/common/helper");
14
11
  const console_log_colors_1 = require("console-log-colors");
15
- const ffmpeg_static_1 = __importDefault(require("ffmpeg-static"));
12
+ const i18n_js_1 = require("./i18n.js");
16
13
  const format_options_js_1 = require("./format-options.js");
17
14
  const local_play_js_1 = require("./local-play.js");
18
15
  const m3u8_convert_js_1 = require("./m3u8-convert.js");
@@ -91,7 +88,7 @@ const cache = {
91
88
  const tsDlFile = (0, node_path_1.resolve)(__dirname, './ts-download.js');
92
89
  exports.workPollPublic = new worker_pool_js_1.WorkerPool(tsDlFile);
93
90
  async function m3u8InfoParse(u, o = {}) {
94
- const ffmpegBin = o.useGlobalFfmpeg ? 'ffmpeg' : ffmpeg_static_1.default;
91
+ const ffmpegBin = o.ffmpegPath || 'ffmpeg';
95
92
  const ext = (0, utils_js_1.isSupportFfmpeg)(ffmpegBin) ? '.mp4' : '.ts';
96
93
  const { url, options, urlMd5 } = (0, format_options_js_1.formatOptions)(u, o);
97
94
  /** 最终合并转换后的文件路径 */
@@ -105,17 +102,32 @@ async function m3u8InfoParse(u, o = {}) {
105
102
  }
106
103
  if (!options.force && (0, node_fs_1.existsSync)(filepath))
107
104
  return result;
105
+ const lang = (0, i18n_js_1.getLang)(o.lang);
108
106
  const m3u8Info = await (0, parseM3u8_js_1.parseM3U8)(url, (0, node_path_1.resolve)(options.cacheDir, urlMd5), options.headers).catch(e => {
109
- 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);
110
108
  console.log(e);
111
109
  });
112
110
  if (m3u8Info && m3u8Info?.tsCount > 0) {
113
111
  result.m3u8Info = m3u8Info;
114
112
  if (options.ignoreSegments) {
113
+ const totalDuration = m3u8Info.duration;
115
114
  const timeSegments = options.ignoreSegments
116
115
  .split(',')
117
- .map(d => d.split(/[- ]+/).map(d => +d.trim()))
118
- .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]);
119
131
  if (timeSegments.length) {
120
132
  const total = m3u8Info.data.length;
121
133
  m3u8Info.data = m3u8Info.data.filter(item => {
@@ -132,7 +144,7 @@ async function m3u8InfoParse(u, o = {}) {
132
144
  const ignoredCount = total - m3u8Info.data.length;
133
145
  if (ignoredCount) {
134
146
  m3u8Info.tsCount = m3u8Info.data.length;
135
- 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) }));
136
148
  m3u8Info.duration = +Number(m3u8Info.duration).toFixed(2);
137
149
  }
138
150
  }
@@ -189,11 +201,12 @@ async function m3u8Download(url, options = {}) {
189
201
  });
190
202
  }
191
203
  // 原有的下载逻辑
192
- 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));
193
206
  const result = await m3u8InfoParse(url, options);
194
207
  options = result.options;
195
208
  if (!options.force && (0, node_fs_1.existsSync)(result.filepath) && !result.m3u8Info) {
196
- 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);
197
210
  result.isExist = true;
198
211
  return result;
199
212
  }
@@ -231,14 +244,14 @@ async function m3u8Download(url, options = {}) {
231
244
  if (!res || err) {
232
245
  if (err) {
233
246
  console.log('\n');
234
- 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 || '');
235
248
  }
236
249
  if (typeof info.success !== 'number')
237
250
  info.success = 0;
238
251
  else
239
252
  info.success--;
240
253
  if (info.success >= -3) {
241
- 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);
242
255
  setTimeout(() => runTask([info]), 1000);
243
256
  return;
244
257
  }
@@ -293,7 +306,7 @@ async function m3u8Download(url, options = {}) {
293
306
  }
294
307
  };
295
308
  if (options.showProgress) {
296
- 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) }));
297
310
  }
298
311
  result.stats = stats;
299
312
  (0, local_play_js_1.toLocalM3u8)(m3u8Info.data);
@@ -303,16 +316,24 @@ async function m3u8Download(url, options = {}) {
303
316
  await barrier.wait();
304
317
  workPoll.close();
305
318
  if (stats.tsFailed > 0) {
306
- 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);
307
322
  }
308
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);
309
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);
310
329
  if (result.filepath && (0, node_fs_1.existsSync)(result.filepath)) {
311
330
  stats.size = (0, node_fs_1.statSync)(result.filepath).size;
312
331
  if (options.delCache)
313
332
  (0, fe_utils_1.rmrfAsync)((0, node_path_1.dirname)(m3u8Info.data[0].tsOut));
314
333
  }
315
334
  }
335
+ if (options.onProgress)
336
+ options.onProgress(stats.tsCount, m3u8Info.tsCount, null, stats);
316
337
  }
317
338
  utils_js_1.logger.debug('Done!', url, result.m3u8Info);
318
339
  return result;
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;
26
+ const ffmpegTestCache = {};
27
27
  function isSupportFfmpeg(ffmpegBin) {
28
- if (null == _isSupportFfmpeg)
29
- _isSupportFfmpeg = (0, fe_utils_1.execSync)(`${ffmpegBin} -version`).stderr === '';
30
- return _isSupportFfmpeg;
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,8 +140,10 @@ export interface M3u8DLOptions {
140
140
  * - 'parser':下载 VideoParser 支持解析的平台视频文件
141
141
  */
142
142
  type?: 'm3u8' | 'file' | 'parser';
143
- /** 是否使用系统安装的 ffmpeg 而不是内置的 ffmpeg-static。默认为 false */
144
- useGlobalFfmpeg?: boolean;
143
+ /** ffmpeg 可执行文件路径。如果未指定,则尝试使用系统 PATH 中的 'ffmpeg' */
144
+ ffmpegPath?: string;
145
+ /** 语言。可选值:zh, en */
146
+ lang?: 'zh' | 'en';
145
147
  }
146
148
  export interface M3u8DLResult extends Partial<DownloadResult> {
147
149
  /** 下载进度统计 */