@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.
- package/README.MD +361 -68
- package/cjs/cli.js +37 -24
- package/cjs/i18n/locales/en.d.ts +108 -0
- package/cjs/i18n/locales/en.js +109 -0
- package/cjs/i18n/locales/zh.d.ts +108 -0
- package/cjs/i18n/locales/zh.js +109 -0
- package/cjs/index.d.ts +1 -1
- package/cjs/index.js +1 -1
- package/cjs/lib/file-download.js +8 -5
- package/cjs/lib/getM3u8Urls.d.ts +6 -0
- package/cjs/lib/getM3u8Urls.js +45 -27
- package/cjs/lib/i18n.d.ts +27 -0
- package/cjs/lib/i18n.js +89 -0
- package/cjs/lib/m3u8-convert.js +2 -7
- package/cjs/lib/m3u8-download.js +36 -15
- package/cjs/lib/utils.js +4 -4
- package/cjs/server/download-server.d.ts +1 -0
- package/cjs/server/download-server.js +111 -39
- package/cjs/types/index.d.ts +1 -1
- package/cjs/types/index.js +1 -1
- package/cjs/types/m3u8.d.ts +4 -2
- package/client/assets/main-DYJAIw1q.css +1 -0
- package/client/assets/main-XL0wiaDU.js +25 -0
- package/client/index.html +4 -1144
- package/client/play.html +221 -14
- package/package.json +20 -17
- package/client/style.css +0 -137
package/cjs/lib/i18n.js
ADDED
|
@@ -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
|
+
}
|
package/cjs/lib/m3u8-convert.js
CHANGED
|
@@ -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
|
|
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 \`${
|
|
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
|
}
|
package/cjs/lib/m3u8-download.js
CHANGED
|
@@ -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
|
|
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.
|
|
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('
|
|
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 =>
|
|
118
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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('
|
|
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(
|
|
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(`\
|
|
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
|
-
|
|
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
|
-
|
|
26
|
+
const ffmpegTestCache = {};
|
|
27
27
|
function isSupportFfmpeg(ffmpegBin) {
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
return
|
|
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 = [];
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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(
|
|
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: '
|
|
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
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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', (
|
|
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
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
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 })
|
package/cjs/types/index.d.ts
CHANGED
package/cjs/types/index.js
CHANGED
|
@@ -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);
|
package/cjs/types/m3u8.d.ts
CHANGED
|
@@ -140,8 +140,10 @@ export interface M3u8DLOptions {
|
|
|
140
140
|
* - 'parser':下载 VideoParser 支持解析的平台视频文件
|
|
141
141
|
*/
|
|
142
142
|
type?: 'm3u8' | 'file' | 'parser';
|
|
143
|
-
/**
|
|
144
|
-
|
|
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
|
/** 下载进度统计 */
|