@lzwme/m3u8-dl 1.5.0 → 1.6.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,27 @@
1
+ /**
2
+ * Shared i18n utility for backend (CLI, SDK, Server)
3
+ */
4
+ import type { AnyObject } from '@lzwme/fe-utils';
5
+ type Locale = 'zh-CN' | 'en';
6
+ export declare const LANG_CODES: Set<string>;
7
+ /**
8
+ * Detect language from OS or environment
9
+ */
10
+ export declare function detectLanguage(): Locale;
11
+ /**
12
+ * Set global language context
13
+ */
14
+ export declare function setLanguage(lang: Locale | null): void;
15
+ /**
16
+ * Get global language context
17
+ */
18
+ export declare function getLanguage(): Locale | null;
19
+ /**
20
+ * Get language from various sources
21
+ */
22
+ export declare function getLang(lang?: string): Locale;
23
+ /**
24
+ * Translation function
25
+ */
26
+ export declare function t(key: string, lang?: string, params?: AnyObject): string;
27
+ export {};
@@ -0,0 +1,108 @@
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-CN', '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();
22
+ // 支持 zh-CN, zh-TW, zh 等变体映射到 zh-CN
23
+ if (langCode.startsWith('zh')) {
24
+ return 'zh-CN';
25
+ }
26
+ const baseLangCode = langCode.split('-')[0].split('_')[0];
27
+ if (exports.LANG_CODES.has(baseLangCode)) {
28
+ return baseLangCode;
29
+ }
30
+ }
31
+ // Try to detect from OS locale
32
+ try {
33
+ const osLocale = Intl.DateTimeFormat().resolvedOptions().locale;
34
+ const langCode = osLocale.toLowerCase();
35
+ // 支持 zh-CN, zh-TW 等变体映射到 zh-CN
36
+ if (langCode.startsWith('zh')) {
37
+ return 'zh-CN';
38
+ }
39
+ const baseLangCode = langCode.split('-')[0];
40
+ if (exports.LANG_CODES.has(baseLangCode)) {
41
+ return baseLangCode;
42
+ }
43
+ }
44
+ catch {
45
+ // Ignore errors
46
+ }
47
+ // Fallback to English
48
+ return 'en';
49
+ }
50
+ /**
51
+ * Set global language context
52
+ */
53
+ function setLanguage(lang) {
54
+ globalLang = lang;
55
+ }
56
+ /**
57
+ * Get global language context
58
+ */
59
+ function getLanguage() {
60
+ return globalLang;
61
+ }
62
+ /**
63
+ * Get language from various sources
64
+ */
65
+ function getLang(lang) {
66
+ if (lang) {
67
+ const normalizedLang = lang.toLowerCase();
68
+ // 支持向后兼容:将 zh 映射到 zh-CN
69
+ if (normalizedLang === 'zh' || normalizedLang.startsWith('zh')) {
70
+ return 'zh-CN';
71
+ }
72
+ if (exports.LANG_CODES.has(normalizedLang)) {
73
+ return normalizedLang;
74
+ }
75
+ }
76
+ if (globalLang) {
77
+ return globalLang;
78
+ }
79
+ return detectLanguage();
80
+ }
81
+ /**
82
+ * Translation function
83
+ */
84
+ function t(key, lang, params) {
85
+ const targetLang = getLang(lang);
86
+ // 将 zh-CN 映射到文件名 zh-CN.ts
87
+ const langFile = targetLang === 'zh-CN' ? 'zh-CN' : targetLang;
88
+ const translations = require(`../i18n/locales/${langFile}.js`);
89
+ // Navigate through nested keys (e.g., 'cli.command.download.description')
90
+ const keys = key.split('.');
91
+ let value = translations.default || translations;
92
+ for (const k of keys) {
93
+ if (value && typeof value === 'object' && k in value) {
94
+ value = value[k];
95
+ }
96
+ else {
97
+ // Key not found, return the key itself
98
+ return key;
99
+ }
100
+ }
101
+ // If value is a string, replace params if provided
102
+ if (typeof value === 'string' && params) {
103
+ return value.replace(/\{(\w+)\}/g, (match, paramKey) => {
104
+ return params[paramKey] !== undefined ? String(params[paramKey]) : match;
105
+ });
106
+ }
107
+ return typeof value === 'string' ? value : key;
108
+ }
@@ -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,8 +9,8 @@ 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"));
16
12
  const format_options_js_1 = require("./format-options.js");
13
+ const i18n_js_1 = require("./i18n.js");
17
14
  const local_play_js_1 = require("./local-play.js");
18
15
  const m3u8_convert_js_1 = require("./m3u8-convert.js");
19
16
  const parseM3u8_js_1 = require("./parseM3u8.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,29 @@ 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
+ const isApi = req.path.startsWith('/api/');
213
+ if (!isApi && (isIndexPage || isPlayPage)) {
196
214
  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);
215
+ let htmlContent = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(rootDir, `client/${isPlayPage ? 'play' : 'index'}.html`), 'utf-8').replaceAll('{{version}}', version);
198
216
  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');
217
+ // 提取所有 zstatic.net 的 js 和 css 资源地址,若子路径存在于 local/cdn 目录下则替换为本地路径
218
+ const zstaticRegex = /https:\/\/s4\.zstatic\.net\/ajax\/libs\/[^\s"'`<>]+\.(js|css)/g;
219
+ const zstaticMatches = htmlContent.match(zstaticRegex);
220
+ if (zstaticMatches) {
221
+ for (const match of zstaticMatches) {
222
+ const relativePath = match.split('libs/')[1];
223
+ const localPath = (0, node_path_1.resolve)(rootDir, `client/local/cdn/${relativePath}`);
224
+ if ((0, node_fs_1.existsSync)(localPath)) {
225
+ htmlContent = htmlContent.replaceAll(match, `/local/cdn/${relativePath}`);
226
+ }
227
+ }
228
+ htmlContent = htmlContent.replaceAll(/integrity="[^"]+"\n?/g, '');
229
+ }
203
230
  }
204
- res.setHeader('content-type', 'text/html').send(indexHtml);
231
+ res.setHeader('content-type', 'text/html').send(htmlContent);
205
232
  }
206
233
  else {
207
234
  next();
@@ -221,9 +248,10 @@ class DLServer {
221
248
  if (this.options.token && req.headers.authorization !== this.options.token) {
222
249
  const ignorePaths = ['/healthcheck', '/localplay'];
223
250
  if (!ignorePaths.some(d => req.url.includes(d))) {
251
+ const lang = this.getLangFromRequest(req);
224
252
  const clientIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
225
253
  utils_js_1.logger.warn('Unauthorized access:', clientIp, req.url, req.headers.authorization);
226
- res.status(401).json({ message: '未授权,禁止访问', code: 401 });
254
+ res.status(401).json({ message: (0, i18n_js_1.t)('api.error.unauthorized', lang), code: 1008 });
227
255
  return;
228
256
  }
229
257
  }
@@ -338,6 +366,28 @@ class DLServer {
338
366
  this.wsSend('progress', nextItem[0]);
339
367
  }
340
368
  }
369
+ getLangFromRequest(req) {
370
+ // Try to get lang from query parameter
371
+ const queryLang = req.query?.lang;
372
+ if (queryLang && i18n_js_1.LANG_CODES.has(queryLang)) {
373
+ return queryLang;
374
+ }
375
+ // Try to get lang from Accept-Language header
376
+ const acceptLanguage = req.headers['accept-language'];
377
+ if (acceptLanguage) {
378
+ const langCode = acceptLanguage.toLowerCase().split(',')[0].split('-')[0].trim();
379
+ if (i18n_js_1.LANG_CODES.has(langCode)) {
380
+ return langCode;
381
+ }
382
+ }
383
+ // Try to get lang from body
384
+ const bodyLang = req.body?.lang;
385
+ if (bodyLang && i18n_js_1.LANG_CODES.has(bodyLang)) {
386
+ return bodyLang;
387
+ }
388
+ // Fallback to default
389
+ return (0, i18n_js_1.getLang)();
390
+ }
341
391
  wsSend(type = 'progress', data) {
342
392
  if (type === 'tasks' && !data) {
343
393
  data = Object.fromEntries(this.dlCacheClone());
@@ -361,29 +411,40 @@ class DLServer {
361
411
  res.json({ message: 'ok', code: 0 });
362
412
  });
363
413
  app.post('/api/config', (req, res) => {
364
- const config = req.body;
365
- this.saveConfig(config);
366
- res.json({ message: 'Config updated successfully', code: 0 });
414
+ const lang = this.getLangFromRequest(req);
415
+ try {
416
+ const config = req.body;
417
+ this.saveConfig(config);
418
+ res.json({ message: (0, i18n_js_1.t)('api.success.configUpdated', lang), code: 0 });
419
+ }
420
+ catch (error) {
421
+ const errorMessage = error instanceof Error ? error.message : (0, i18n_js_1.t)('api.error.configSaveFailed', lang);
422
+ utils_js_1.logger.error('[saveConfig]', errorMessage);
423
+ res.status(400).json({ message: errorMessage, code: 1 });
424
+ }
367
425
  });
368
426
  app.get('/api/config', (_req, res) => {
369
- res.json({ ...this.cfg.dlOptions, ...this.cfg.webOptions });
427
+ res.json({ code: 0, data: { ...this.cfg.dlOptions, ...this.cfg.webOptions } });
370
428
  });
371
429
  // API to get all download progress
372
430
  app.get('/api/tasks', (_req, res) => {
373
- res.json(Object.fromEntries(this.dlCacheClone()));
431
+ res.json({ code: 0, data: Object.fromEntries(this.dlCacheClone()) });
374
432
  });
375
433
  // API to get queue status
376
434
  app.get('/api/queue/status', (_req, res) => {
377
435
  const pendingTasks = Array.from(this.dlCache.entries()).filter(([_, item]) => item.status === 'pending');
378
436
  const activeTasks = Array.from(this.dlCache.entries()).filter(([_, item]) => item.status === 'resume');
379
437
  res.json({
380
- queueLength: pendingTasks.length,
381
- activeDownloads: activeTasks.map(([url]) => url),
382
- maxConcurrent: this.cfg.webOptions.maxDownloads,
438
+ code: 0,
439
+ data: {
440
+ queueLength: pendingTasks.length,
441
+ activeDownloads: activeTasks.map(([url]) => url),
442
+ maxConcurrent: this.cfg.webOptions.maxDownloads,
443
+ },
383
444
  });
384
445
  });
385
446
  // API to clear queue
386
- app.post('/api/queue/clear', (_req, res) => {
447
+ app.post('/api/queue/clear', (req, res) => {
387
448
  let count = 0;
388
449
  for (const [url, item] of this.dlCache.entries()) {
389
450
  if (item.status === 'pending') {
@@ -393,23 +454,25 @@ class DLServer {
393
454
  }
394
455
  if (count)
395
456
  this.wsSend('tasks');
396
- res.json({ message: `已清空 ${count} 个等待中的下载任务`, code: 0 });
457
+ const lang = this.getLangFromRequest(req);
458
+ res.json({ message: (0, i18n_js_1.t)('api.success.queueCleared', lang, { count }), code: 0 });
397
459
  });
398
460
  // 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
- });
461
+ // app.post('/api/priority', (req, res) => {
462
+ // const { url, priority } = req.body;
463
+ // const item = this.dlCache.get(url);
464
+ // if (!item) {
465
+ // res.json({ message: '任务不存在', code: 1 });
466
+ // return;
467
+ // }
468
+ // item.options.priority = priority;
469
+ // this.saveCache();
470
+ // res.json({ message: '已更新任务优先级', code: 0 });
471
+ // });
410
472
  // API to start m3u8 download
411
473
  app.post('/api/download', (req, res) => {
412
474
  const { url, options = {}, list = [] } = req.body;
475
+ const lang = this.getLangFromRequest(req);
413
476
  try {
414
477
  if (list.length) {
415
478
  for (const item of list) {
@@ -420,11 +483,11 @@ class DLServer {
420
483
  }
421
484
  else if (url)
422
485
  this.startDownload(url, options);
423
- res.json({ message: `Download started: ${list.length || 1}`, code: 0 });
486
+ res.json({ message: (0, i18n_js_1.t)('api.success.downloadStarted', lang, { count: list.length || 1 }), code: 0 });
424
487
  this.wsSend('tasks');
425
488
  }
426
489
  catch (error) {
427
- res.status(500).json({ error: `Download failed: ${error.message}` });
490
+ res.status(500).json({ error: `${(0, i18n_js_1.t)('api.error.downloadFailed', lang)}: ${error.message || ''}` });
428
491
  }
429
492
  });
430
493
  // API to pause download
@@ -445,7 +508,8 @@ class DLServer {
445
508
  this.wsSend('progress', list);
446
509
  this.startNextPending();
447
510
  }
448
- res.json({ message: `已暂停 ${list.length} 个下载任务`, code: 0, count: list.length });
511
+ const lang = this.getLangFromRequest(req);
512
+ res.json({ message: (0, i18n_js_1.t)('api.success.paused', lang, { count: list.length }), code: 0, count: list.length });
449
513
  });
450
514
  // API to resume download
451
515
  app.post('/api/resume', (req, res) => {
@@ -464,7 +528,12 @@ class DLServer {
464
528
  }
465
529
  if (list.length)
466
530
  this.wsSend('progress', list);
467
- res.json({ message: list.length ? `已恢复 ${list.length} 个下载任务` : '没有找到可恢复的下载任务', code: 0, count: list.length });
531
+ const lang = this.getLangFromRequest(req);
532
+ res.json({
533
+ message: list.length ? (0, i18n_js_1.t)('api.success.resumed', lang, { count: list.length }) : (0, i18n_js_1.t)('api.success.noResumableTasks', lang),
534
+ code: 0,
535
+ count: list.length,
536
+ });
468
537
  });
469
538
  // API to delete download
470
539
  app.post('/api/delete', (req, res) => {
@@ -500,7 +569,8 @@ class DLServer {
500
569
  this.saveCache();
501
570
  this.startNextPending();
502
571
  }
503
- res.json({ message: `已删除 ${list.length} 个下载任务`, code: 0, count: list.length });
572
+ const lang = this.getLangFromRequest(req);
573
+ res.json({ message: (0, i18n_js_1.t)('api.success.deleted', lang, { count: list.length }), code: 0, count: list.length });
504
574
  });
505
575
  app.get(/^\/localplay\/(.*)$/, (req, res) => {
506
576
  let filepath = decodeURIComponent(req.params[0]);
@@ -526,7 +596,8 @@ class DLServer {
526
596
  const isAllow = !this.options.limitFileAccess || allowedDirs.some(d => filepath.startsWith((0, node_path_1.resolve)(d)));
527
597
  if (!isAllow) {
528
598
  utils_js_1.logger.error('[Localplay] Access denied:', filepath);
529
- res.send({ message: 'Access denied', code: 403 });
599
+ const lang = this.getLangFromRequest(req);
600
+ res.send({ message: (0, i18n_js_1.t)('api.error.accessDenied', lang), code: 403 });
530
601
  return;
531
602
  }
532
603
  }
@@ -553,12 +624,14 @@ class DLServer {
553
624
  }
554
625
  }
555
626
  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 });
627
+ const lang = this.getLangFromRequest(req);
628
+ res.status(404).send({ message: (0, i18n_js_1.t)('api.error.notFound', lang), code: 404 });
557
629
  });
558
630
  app.post('/api/getM3u8Urls', (req, res) => {
559
631
  const { url, headers, subUrlRegex } = req.body;
632
+ const lang = this.getLangFromRequest(req);
560
633
  if (!url) {
561
- res.json({ code: 1001, message: '无效的 url 参数' });
634
+ res.json({ code: 1001, message: (0, i18n_js_1.t)('api.error.invalidUrl', lang) });
562
635
  }
563
636
  else {
564
637
  (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-CN, en */
146
+ lang?: 'zh-CN' | 'en';
145
147
  }
146
148
  export interface M3u8DLResult extends Partial<DownloadResult> {
147
149
  /** 下载进度统计 */