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