@lzwme/m3u8-dl 1.1.2 → 1.2.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 +2 -2
- package/cjs/cli.js +1 -0
- package/cjs/index.d.ts +2 -0
- package/cjs/index.js +2 -0
- package/cjs/lib/file-download.d.ts +2 -0
- package/cjs/lib/file-download.js +81 -0
- package/cjs/lib/format-options.d.ts +2 -0
- package/cjs/lib/format-options.js +80 -0
- package/cjs/lib/m3u8-download.d.ts +2 -10
- package/cjs/lib/m3u8-download.js +13 -6
- package/cjs/lib/utils.d.ts +0 -2
- package/cjs/lib/utils.js +0 -37
- package/cjs/m3u8-batch-download.js +29 -24
- package/cjs/server/download-server.d.ts +3 -0
- package/cjs/server/download-server.js +41 -60
- package/cjs/types/m3u8.d.ts +18 -6
- package/cjs/types/video-parser.d.ts +7 -7
- package/cjs/video-parser/index.d.ts +2 -5
- package/cjs/video-parser/index.js +14 -74
- package/cjs/video-parser/parsers/base-parser.d.ts +1 -1
- package/cjs/video-parser/parsers/base-parser.js +4 -12
- package/client/index.html +15 -6
- package/package.json +1 -1
package/README.MD
CHANGED
|
@@ -128,13 +128,13 @@ for (const filepath of fileList) {
|
|
|
128
128
|
|
|
129
129
|
```bash
|
|
130
130
|
# docker pull ghcr.io/lzwme/m3u8-dl:latest
|
|
131
|
-
docker pull
|
|
131
|
+
docker pull renxia/m3u8dl-dl:latest
|
|
132
132
|
|
|
133
133
|
docker run --rm -it \
|
|
134
134
|
-v ./cache:/app/cache \
|
|
135
135
|
-v ./downloads:/app/downloads \
|
|
136
136
|
-p 6600:6600 \
|
|
137
|
-
|
|
137
|
+
renxia/m3u8dl-dl:latest
|
|
138
138
|
```
|
|
139
139
|
|
|
140
140
|
也可以基于 [docker-compose.yml](./docker/docker-compose.yml) 部署:
|
package/cjs/cli.js
CHANGED
|
@@ -66,6 +66,7 @@ commander_1.program
|
|
|
66
66
|
.option('--no-del-cache', `下载成功后是否删除临时文件。默认为 true。保存临时文件可以在重复下载时识别缓存`)
|
|
67
67
|
.option('--no-convert', '下载成功后,是否不合并转换为 mp4 文件。默认为 true。')
|
|
68
68
|
.option('-H, --headers <headers>', `自定义请求头。格式为 key1=value1\nkey2=value2`)
|
|
69
|
+
.option('-T, --type <type>', `指定下载类型。默认根据URL自动识别,如果是批量下载多个不同 URL 类型,请不要设置。可选值:m3u8, file, parser`)
|
|
69
70
|
.action(async (urls) => {
|
|
70
71
|
const options = getOptions();
|
|
71
72
|
utils_js_1.logger.debug(urls, options);
|
package/cjs/index.d.ts
CHANGED
package/cjs/index.js
CHANGED
|
@@ -15,4 +15,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./lib/m3u8-download"), exports);
|
|
18
|
+
__exportStar(require("./lib/file-download"), exports);
|
|
18
19
|
__exportStar(require("./lib/parseM3u8"), exports);
|
|
20
|
+
__exportStar(require("./video-parser"), exports);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fileDownload = fileDownload;
|
|
4
|
+
const node_path_1 = require("node:path");
|
|
5
|
+
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
6
|
+
const console_log_colors_1 = require("console-log-colors");
|
|
7
|
+
const utils_js_1 = require("./utils.js");
|
|
8
|
+
const format_options_js_1 = require("./format-options.js");
|
|
9
|
+
async function fileDownload(url, options) {
|
|
10
|
+
utils_js_1.logger.debug('fileDownload', url, options);
|
|
11
|
+
[url, options] = (0, format_options_js_1.formatOptions)(url, options);
|
|
12
|
+
const startTime = Date.now();
|
|
13
|
+
const stats = {
|
|
14
|
+
url,
|
|
15
|
+
startTime,
|
|
16
|
+
progress: 0,
|
|
17
|
+
tsSuccess: 0,
|
|
18
|
+
tsFailed: 0,
|
|
19
|
+
tsCount: 0,
|
|
20
|
+
duration: 0,
|
|
21
|
+
durationDownloaded: 0,
|
|
22
|
+
downloadedSize: 0,
|
|
23
|
+
avgSpeed: 0,
|
|
24
|
+
avgSpeedDesc: '0B/s',
|
|
25
|
+
speed: 0,
|
|
26
|
+
speedDesc: '0B/s',
|
|
27
|
+
remainingTime: 0,
|
|
28
|
+
filename: options.filename,
|
|
29
|
+
localVideo: (0, node_path_1.resolve)(options.saveDir, options.filename),
|
|
30
|
+
};
|
|
31
|
+
utils_js_1.logger.debug('开始下载', (0, console_log_colors_1.gray)(url), (0, console_log_colors_1.cyan)(stats.localVideo));
|
|
32
|
+
if (options.onInited)
|
|
33
|
+
options.onInited(stats, null, null);
|
|
34
|
+
try {
|
|
35
|
+
const r = await (0, fe_utils_1.download)({
|
|
36
|
+
url,
|
|
37
|
+
filepath: stats.localVideo,
|
|
38
|
+
paralelism: options.threadNum,
|
|
39
|
+
force: options.force,
|
|
40
|
+
requestOptions: {
|
|
41
|
+
headers: {
|
|
42
|
+
referer: url,
|
|
43
|
+
...(0, utils_js_1.formatHeaders)(options.headers),
|
|
44
|
+
},
|
|
45
|
+
rejectUnauthorized: false,
|
|
46
|
+
},
|
|
47
|
+
onProgress: info => {
|
|
48
|
+
stats.progress = +info.percent.toFixed(2);
|
|
49
|
+
stats.size = info.size;
|
|
50
|
+
stats.downloadedSize = info.downloaded;
|
|
51
|
+
stats.speed = info.speed;
|
|
52
|
+
stats.speedDesc = (0, fe_utils_1.formatByteSize)(stats.speed) + '/s';
|
|
53
|
+
stats.remainingTime = Math.round((info.size - info.downloaded) / stats.speed);
|
|
54
|
+
if (options.showProgress) {
|
|
55
|
+
const processBar = info.percent === -1 ? '' : '='.repeat(Math.floor(stats.progress * 0.2)).padEnd(20, '-');
|
|
56
|
+
utils_js_1.logger.logInline(`${stats.progress}% [${(0, console_log_colors_1.greenBright)(processBar)}] ` +
|
|
57
|
+
`${(0, console_log_colors_1.blueBright)((0, fe_utils_1.formatByteSize)(stats.downloadedSize))} ${(0, console_log_colors_1.yellowBright)((0, fe_utils_1.formatTimeCost)(startTime))} ${(0, console_log_colors_1.magentaBright)(stats.speedDesc)} ` +
|
|
58
|
+
(stats.progress === 100 ? '\n' : stats.remainingTime ? `${(0, console_log_colors_1.cyan)((0, fe_utils_1.formatTimeCost)(Date.now() - stats.remainingTime))}` : ''));
|
|
59
|
+
}
|
|
60
|
+
if (options.onProgress) {
|
|
61
|
+
return options.onProgress(info.downloaded, info.size, null, stats);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
stats.endTime = Date.now();
|
|
66
|
+
return {
|
|
67
|
+
errmsg: r.filepath ? '下载完成' : '下载失败',
|
|
68
|
+
...r,
|
|
69
|
+
stats,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
utils_js_1.logger.error('下载失败', error.message, (0, console_log_colors_1.gray)(url));
|
|
74
|
+
stats.errmsg = error.message;
|
|
75
|
+
return {
|
|
76
|
+
isExist: false,
|
|
77
|
+
errmsg: '下载失败: ' + error.message,
|
|
78
|
+
stats,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatOptions = formatOptions;
|
|
4
|
+
const node_os_1 = require("node:os");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
7
|
+
const utils_1 = require("./utils");
|
|
8
|
+
const video_parser_1 = require("../video-parser");
|
|
9
|
+
const fileSupportExtList = [
|
|
10
|
+
'.mp4',
|
|
11
|
+
'.mkv',
|
|
12
|
+
'.avi',
|
|
13
|
+
'.mov',
|
|
14
|
+
'.wmv',
|
|
15
|
+
'.ts',
|
|
16
|
+
'.exe',
|
|
17
|
+
'.zip',
|
|
18
|
+
'.rar',
|
|
19
|
+
'.pdf',
|
|
20
|
+
'.doc',
|
|
21
|
+
'.docx',
|
|
22
|
+
'.xls',
|
|
23
|
+
'.xlsx',
|
|
24
|
+
'.ppt',
|
|
25
|
+
'.pptx',
|
|
26
|
+
];
|
|
27
|
+
function formatOptions(url, opts) {
|
|
28
|
+
const options = {
|
|
29
|
+
delCache: !opts.debug,
|
|
30
|
+
saveDir: process.cwd(),
|
|
31
|
+
showProgress: true,
|
|
32
|
+
...opts,
|
|
33
|
+
};
|
|
34
|
+
let ext = options.filename ? (0, node_path_1.extname)(options.filename) : '';
|
|
35
|
+
if (!options.type) {
|
|
36
|
+
if (video_parser_1.VideoParser.getPlatform(url).platform !== 'unknown') {
|
|
37
|
+
options.type = 'parser';
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
options.type = 'm3u8';
|
|
41
|
+
if (!url.includes('.m3u8')) {
|
|
42
|
+
const e = fileSupportExtList.find(d => url.includes(d));
|
|
43
|
+
if (e) {
|
|
44
|
+
options.type = 'file';
|
|
45
|
+
ext = e;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let [u, n] = url.split(/[|$]+/);
|
|
51
|
+
if (n && n.startsWith('http'))
|
|
52
|
+
[u, n] = [n, u];
|
|
53
|
+
url = u;
|
|
54
|
+
if (n) {
|
|
55
|
+
if (!options.filename)
|
|
56
|
+
options.filename = n;
|
|
57
|
+
else
|
|
58
|
+
options.filename = `${options.filename.replace(/\.(ts|mp4)$/, '')}-${n}${ext}`;
|
|
59
|
+
}
|
|
60
|
+
const urlMd5 = (0, fe_utils_1.md5)(url, false);
|
|
61
|
+
if (!options.filename) {
|
|
62
|
+
if (ext && url.includes(ext))
|
|
63
|
+
options.filename = (0, node_path_1.basename)(url.split(ext)[0]) + ext;
|
|
64
|
+
else
|
|
65
|
+
options.filename = urlMd5 + ext;
|
|
66
|
+
}
|
|
67
|
+
if (!(0, node_path_1.extname)(options.filename) && options.type !== 'file')
|
|
68
|
+
options.filename += ext || '.mp4';
|
|
69
|
+
if (!options.cacheDir)
|
|
70
|
+
options.cacheDir = `cache`;
|
|
71
|
+
if (options.headers)
|
|
72
|
+
options.headers = (0, utils_1.formatHeaders)(options.headers);
|
|
73
|
+
if (!options.threadNum || +options.threadNum <= 0)
|
|
74
|
+
options.threadNum = Math.min((0, node_os_1.cpus)().length * 2, 8);
|
|
75
|
+
if (options.debug) {
|
|
76
|
+
utils_1.logger.updateOptions({ levelType: 'debug' });
|
|
77
|
+
utils_1.logger.debug('[m3u8-DL]options', options, url);
|
|
78
|
+
}
|
|
79
|
+
return [url, options, urlMd5];
|
|
80
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { M3u8DLOptions, M3u8WorkerPool } from '../types/m3u8.js';
|
|
1
|
+
import type { M3u8DLOptions, M3u8DLResult, M3u8WorkerPool } from '../types/m3u8.js';
|
|
3
2
|
/** 下载队列管理 */
|
|
4
3
|
export declare class DownloadQueue {
|
|
5
4
|
private queue;
|
|
@@ -23,12 +22,5 @@ export declare class DownloadQueue {
|
|
|
23
22
|
export declare const downloadQueue: DownloadQueue;
|
|
24
23
|
export declare const workPollPublic: M3u8WorkerPool;
|
|
25
24
|
export declare function preDownLoad(url: string, options: M3u8DLOptions, wp?: M3u8WorkerPool): Promise<void>;
|
|
26
|
-
export declare function m3u8Download(url: string, options?: M3u8DLOptions): Promise<
|
|
27
|
-
filepath?: string;
|
|
28
|
-
error?: Error;
|
|
29
|
-
} | {
|
|
30
|
-
options: M3u8DLOptions;
|
|
31
|
-
m3u8Info: Awaited<ReturnType<typeof parseM3U8>> | null;
|
|
32
|
-
filepath: string;
|
|
33
|
-
}>;
|
|
25
|
+
export declare function m3u8Download(url: string, options?: M3u8DLOptions): Promise<M3u8DLResult>;
|
|
34
26
|
export declare function m3u8DLStop(url: string, wp?: M3u8WorkerPool): number;
|
package/cjs/lib/m3u8-download.js
CHANGED
|
@@ -10,6 +10,7 @@ const fe_utils_1 = require("@lzwme/fe-utils");
|
|
|
10
10
|
const helper_1 = require("@lzwme/fe-utils/cjs/common/helper");
|
|
11
11
|
const console_log_colors_1 = require("console-log-colors");
|
|
12
12
|
const utils_js_1 = require("./utils.js");
|
|
13
|
+
const format_options_js_1 = require("./format-options.js");
|
|
13
14
|
const worker_pool_js_1 = require("./worker_pool.js");
|
|
14
15
|
const parseM3u8_js_1 = require("./parseM3u8.js");
|
|
15
16
|
const m3u8_convert_js_1 = require("./m3u8-convert.js");
|
|
@@ -43,11 +44,12 @@ class DownloadQueue {
|
|
|
43
44
|
try {
|
|
44
45
|
const { maxDownloads, ...options } = next.options;
|
|
45
46
|
const result = await m3u8Download(next.url, options);
|
|
46
|
-
next.options.onComplete?.(
|
|
47
|
+
next.options.onComplete?.(result);
|
|
47
48
|
}
|
|
48
49
|
catch (error) {
|
|
49
50
|
next.options.onComplete?.({
|
|
50
|
-
|
|
51
|
+
errmsg: error instanceof Error ? error.message : error ? JSON.stringify(error) : 'Unknown error',
|
|
52
|
+
options: next.options,
|
|
51
53
|
});
|
|
52
54
|
}
|
|
53
55
|
finally {
|
|
@@ -85,7 +87,8 @@ const cache = {
|
|
|
85
87
|
const tsDlFile = (0, node_path_1.resolve)(__dirname, './ts-download.js');
|
|
86
88
|
exports.workPollPublic = new worker_pool_js_1.WorkerPool(tsDlFile);
|
|
87
89
|
async function m3u8InfoParse(url, options = {}) {
|
|
88
|
-
|
|
90
|
+
let urlMd5 = '';
|
|
91
|
+
[url, options, urlMd5] = (0, format_options_js_1.formatOptions)(url, options);
|
|
89
92
|
const ext = (0, utils_js_1.isSupportFfmpeg)() ? '.mp4' : '.ts';
|
|
90
93
|
/** 最终合并转换后的文件路径 */
|
|
91
94
|
let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename);
|
|
@@ -98,7 +101,7 @@ async function m3u8InfoParse(url, options = {}) {
|
|
|
98
101
|
}
|
|
99
102
|
if (!options.force && (0, node_fs_1.existsSync)(filepath))
|
|
100
103
|
return result;
|
|
101
|
-
const m3u8Info = await (0, parseM3u8_js_1.parseM3U8)(url, (0, node_path_1.resolve)(options.cacheDir,
|
|
104
|
+
const m3u8Info = await (0, parseM3u8_js_1.parseM3U8)(url, (0, node_path_1.resolve)(options.cacheDir, urlMd5), options.headers).catch(e => utils_js_1.logger.error('[parseM3U8][failed]', e));
|
|
102
105
|
if (m3u8Info && m3u8Info?.tsCount > 0)
|
|
103
106
|
result.m3u8Info = m3u8Info;
|
|
104
107
|
return result;
|
|
@@ -142,8 +145,10 @@ async function m3u8Download(url, options = {}) {
|
|
|
142
145
|
return new Promise(resolve => {
|
|
143
146
|
const newOptions = {
|
|
144
147
|
...options,
|
|
145
|
-
onComplete:
|
|
146
|
-
|
|
148
|
+
onComplete: r => {
|
|
149
|
+
if (options.onComplete)
|
|
150
|
+
options.onComplete(r);
|
|
151
|
+
resolve(r);
|
|
147
152
|
},
|
|
148
153
|
};
|
|
149
154
|
exports.downloadQueue.add(url, newOptions, options.priority || 0);
|
|
@@ -155,6 +160,7 @@ async function m3u8Download(url, options = {}) {
|
|
|
155
160
|
options = result.options;
|
|
156
161
|
if (!options.force && (0, node_fs_1.existsSync)(result.filepath) && !result.m3u8Info) {
|
|
157
162
|
utils_js_1.logger.info('file already exist:', result.filepath);
|
|
163
|
+
result.isExist = true;
|
|
158
164
|
return result;
|
|
159
165
|
}
|
|
160
166
|
if (result.m3u8Info?.tsCount > 0) {
|
|
@@ -261,6 +267,7 @@ async function m3u8Download(url, options = {}) {
|
|
|
261
267
|
if (options.showProgress) {
|
|
262
268
|
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)}`);
|
|
263
269
|
}
|
|
270
|
+
result.stats = stats;
|
|
264
271
|
(0, local_play_js_1.toLocalM3u8)(m3u8Info.data, options.filename);
|
|
265
272
|
if (options.onInited)
|
|
266
273
|
options.onInited(stats, m3u8Info, workPoll);
|
package/cjs/lib/utils.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { IncomingHttpHeaders } from 'node:http';
|
|
2
2
|
import { Stats } from 'node:fs';
|
|
3
3
|
import { NLogger, Request } from '@lzwme/fe-utils';
|
|
4
|
-
import { M3u8DLOptions } from '../types';
|
|
5
4
|
export declare const request: Request;
|
|
6
5
|
export declare const getRetry: <T = string>(url: string, headers?: IncomingHttpHeaders, retries?: number) => Promise<{
|
|
7
6
|
data: T;
|
|
@@ -19,4 +18,3 @@ export declare function getLocation(url: string, method?: string): Promise<strin
|
|
|
19
18
|
* 如果 headers 是字符串,会先将其解析为对象;如果 headers 为空,则返回空对象。
|
|
20
19
|
*/
|
|
21
20
|
export declare function formatHeaders(headers: string | IncomingHttpHeaders): Record<string, string>;
|
|
22
|
-
export declare function formatOptions(url: string, opts: M3u8DLOptions): readonly [string, M3u8DLOptions];
|
package/cjs/lib/utils.js
CHANGED
|
@@ -5,8 +5,6 @@ exports.isSupportFfmpeg = isSupportFfmpeg;
|
|
|
5
5
|
exports.findFiles = findFiles;
|
|
6
6
|
exports.getLocation = getLocation;
|
|
7
7
|
exports.formatHeaders = formatHeaders;
|
|
8
|
-
exports.formatOptions = formatOptions;
|
|
9
|
-
const node_os_1 = require("node:os");
|
|
10
8
|
const node_fs_1 = require("node:fs");
|
|
11
9
|
const node_path_1 = require("node:path");
|
|
12
10
|
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
@@ -68,38 +66,3 @@ function formatHeaders(headers) {
|
|
|
68
66
|
return {};
|
|
69
67
|
return (0, fe_utils_1.toLowcaseKeyObject)(headers);
|
|
70
68
|
}
|
|
71
|
-
function formatOptions(url, opts) {
|
|
72
|
-
const options = {
|
|
73
|
-
delCache: !opts.debug,
|
|
74
|
-
saveDir: process.cwd(),
|
|
75
|
-
showProgress: true,
|
|
76
|
-
...opts,
|
|
77
|
-
};
|
|
78
|
-
if (!url.startsWith('http')) {
|
|
79
|
-
url = url.replace(/\$+/, '|').replace(/\|\|+/, '|');
|
|
80
|
-
if (url.includes('|')) {
|
|
81
|
-
const r = url.split('|');
|
|
82
|
-
url = r[1];
|
|
83
|
-
if (!options.filename)
|
|
84
|
-
options.filename = r[0];
|
|
85
|
-
else
|
|
86
|
-
options.filename = `${options.filename.replace(/\.(ts|mp4)$/, '')}-${r[0]}`;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
const urlMd5 = (0, fe_utils_1.md5)(url, false);
|
|
90
|
-
if (!options.threadNum || +options.threadNum <= 0)
|
|
91
|
-
options.threadNum = Math.min((0, node_os_1.cpus)().length * 2, 8);
|
|
92
|
-
if (!options.filename)
|
|
93
|
-
options.filename = urlMd5;
|
|
94
|
-
if (!options.filename.endsWith('.mp4'))
|
|
95
|
-
options.filename += '.mp4';
|
|
96
|
-
if (!options.cacheDir)
|
|
97
|
-
options.cacheDir = `cache`;
|
|
98
|
-
if (options.headers)
|
|
99
|
-
options.headers = formatHeaders(options.headers);
|
|
100
|
-
if (options.debug) {
|
|
101
|
-
exports.logger.updateOptions({ levelType: 'debug' });
|
|
102
|
-
exports.logger.debug('[m3u8-DL]options', options, url);
|
|
103
|
-
}
|
|
104
|
-
return [url, options];
|
|
105
|
-
}
|
|
@@ -5,7 +5,7 @@ exports.m3u8BatchDownload = m3u8BatchDownload;
|
|
|
5
5
|
* @Author: renxia lzwy0820@qq.com
|
|
6
6
|
* @Date: 2024-07-30 08:57:58
|
|
7
7
|
* @LastEditors: renxia
|
|
8
|
-
* @LastEditTime: 2025-05-
|
|
8
|
+
* @LastEditTime: 2025-05-19 17:03:50
|
|
9
9
|
* @FilePath: \m3u8-dl\src\m3u8-batch-download.ts
|
|
10
10
|
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
|
11
11
|
*/
|
|
@@ -15,9 +15,11 @@ const m3u8_download_1 = require("./lib/m3u8-download");
|
|
|
15
15
|
const utils_1 = require("./lib/utils");
|
|
16
16
|
const video_parser_1 = require("./video-parser");
|
|
17
17
|
const console_log_colors_1 = require("console-log-colors");
|
|
18
|
+
const file_download_1 = require("./lib/file-download");
|
|
19
|
+
const format_options_1 = require("./lib/format-options");
|
|
18
20
|
async function formatUrls(urls, options) {
|
|
19
21
|
const taskset = new Map();
|
|
20
|
-
for (
|
|
22
|
+
for (let url of urls) {
|
|
21
23
|
if (!url)
|
|
22
24
|
continue;
|
|
23
25
|
if ((0, node_fs_1.existsSync)(url)) {
|
|
@@ -40,6 +42,7 @@ async function formatUrls(urls, options) {
|
|
|
40
42
|
continue;
|
|
41
43
|
}
|
|
42
44
|
}
|
|
45
|
+
[url, options] = (0, format_options_1.formatOptions)(url, options);
|
|
43
46
|
taskset.set(url, options);
|
|
44
47
|
}
|
|
45
48
|
return taskset;
|
|
@@ -49,50 +52,52 @@ async function m3u8BatchDownload(urls, options) {
|
|
|
49
52
|
let workPoll;
|
|
50
53
|
return new Promise(rs => {
|
|
51
54
|
let preDLing = false;
|
|
55
|
+
const afterDownload = (r, url) => {
|
|
56
|
+
const success = r.filepath && (0, node_fs_1.existsSync)(r.filepath);
|
|
57
|
+
if (success) {
|
|
58
|
+
if (r.isExist)
|
|
59
|
+
utils_1.logger.info('文件已存在:', (0, console_log_colors_1.gray)(r.filepath));
|
|
60
|
+
utils_1.logger.debug('下载完成:', (0, console_log_colors_1.gray)(url), (0, console_log_colors_1.gray)(r.filepath));
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
utils_1.logger.error('下载失败:', (0, console_log_colors_1.red)(r.errmsg || '未知错误'), (0, console_log_colors_1.gray)(url));
|
|
64
|
+
}
|
|
65
|
+
if (tasks.size === 0)
|
|
66
|
+
rs(r.filepath && (0, node_fs_1.existsSync)(r.filepath));
|
|
67
|
+
else
|
|
68
|
+
run();
|
|
69
|
+
};
|
|
52
70
|
const run = () => {
|
|
53
71
|
const [url, urlNext] = [...tasks.keys()];
|
|
54
72
|
if (url) {
|
|
55
|
-
const o =
|
|
73
|
+
const o = tasks.get(url);
|
|
56
74
|
const onProgress = o.onProgress;
|
|
57
|
-
const useVideoParser = video_parser_1.VideoParser.getPlatform(url) !== null;
|
|
58
75
|
tasks.delete(url);
|
|
59
76
|
o.onInited = (s, _i, wp) => {
|
|
60
77
|
if (workPoll)
|
|
61
78
|
workPoll = wp;
|
|
62
|
-
if (
|
|
79
|
+
if (o.type === 'parser') {
|
|
63
80
|
utils_1.logger.info('视频解析完成:', s.filename, (0, console_log_colors_1.gray)(s.url));
|
|
64
81
|
}
|
|
65
82
|
};
|
|
66
83
|
o.onProgress = (finished, total, info, stats) => {
|
|
67
84
|
if (onProgress)
|
|
68
85
|
onProgress(finished, total, info, stats);
|
|
69
|
-
if (
|
|
86
|
+
if (o.type === 'm3u8' && !preDLing && urlNext && tasks.size && workPoll.freeNum > 1 && total - finished < options.threadNum) {
|
|
70
87
|
utils_1.logger.debug('\n[预下载下一集]', 'freeNum:', workPoll.freeNum, 'totalNum:', workPoll.totalNum, 'totalTask:', workPoll.totalTask, tasks.size);
|
|
71
88
|
preDLing = true;
|
|
72
89
|
(0, m3u8_download_1.preDownLoad)(urlNext, options, workPoll).then(() => (preDLing = false));
|
|
73
90
|
}
|
|
74
91
|
};
|
|
75
|
-
if (
|
|
92
|
+
if (o.type === 'parser') {
|
|
76
93
|
const vp = new video_parser_1.VideoParser();
|
|
77
|
-
vp.download(url, o).then(r =>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
else {
|
|
82
|
-
if (r.data?.isExist)
|
|
83
|
-
utils_1.logger.info('文件已存在:', (0, console_log_colors_1.gray)(r.data.filepath));
|
|
84
|
-
utils_1.logger.debug('下载完成:', (0, console_log_colors_1.gray)(url), (0, console_log_colors_1.gray)(r.data.filepath));
|
|
85
|
-
}
|
|
86
|
-
if (tasks.size === 0) {
|
|
87
|
-
rs(r.data.filepath && (0, node_fs_1.existsSync)(r.data.filepath));
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
run();
|
|
91
|
-
}
|
|
92
|
-
});
|
|
94
|
+
vp.download(url, o).then(r => afterDownload(r, url));
|
|
95
|
+
}
|
|
96
|
+
else if (o.type === 'file') {
|
|
97
|
+
(0, file_download_1.fileDownload)(url, o).then(r => afterDownload(r, url));
|
|
93
98
|
}
|
|
94
99
|
else {
|
|
95
|
-
(0, m3u8_download_1.m3u8Download)(url, o).then(r => (
|
|
100
|
+
(0, m3u8_download_1.m3u8Download)(url, o).then(r => afterDownload(r, url));
|
|
96
101
|
}
|
|
97
102
|
}
|
|
98
103
|
};
|
|
@@ -11,7 +11,10 @@ interface DLServerOptions {
|
|
|
11
11
|
}
|
|
12
12
|
interface CacheItem extends Partial<M3u8DLProgressStats> {
|
|
13
13
|
url: string;
|
|
14
|
+
/** 用户设置的参数 */
|
|
14
15
|
options: M3u8DLOptions;
|
|
16
|
+
/** 格式化后实际下载使用的参数 */
|
|
17
|
+
dlOptions?: M3u8DLOptions;
|
|
15
18
|
status: 'pause' | 'resume' | 'done' | 'pending' | 'error';
|
|
16
19
|
current?: TsItemInfo;
|
|
17
20
|
workPoll?: M3u8WorkerPool;
|
|
@@ -34,11 +34,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.DLServer = void 0;
|
|
37
|
-
/*
|
|
38
|
-
* @Author: renxia lzwy0820@qq.com
|
|
39
|
-
* @Date: 2025-05-07 21:03:09
|
|
40
|
-
* @LastEditors: renxia
|
|
41
|
-
*/
|
|
42
37
|
const node_fs_1 = require("node:fs");
|
|
43
38
|
const node_path_1 = require("node:path");
|
|
44
39
|
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
@@ -46,6 +41,8 @@ const m3u8_download_js_1 = require("../lib/m3u8-download.js");
|
|
|
46
41
|
const utils_js_1 = require("../lib/utils.js");
|
|
47
42
|
const index_js_1 = require("../video-parser/index.js");
|
|
48
43
|
const console_log_colors_1 = require("console-log-colors");
|
|
44
|
+
const file_download_js_1 = require("../lib/file-download.js");
|
|
45
|
+
const format_options_js_1 = require("../lib/format-options.js");
|
|
49
46
|
class DLServer {
|
|
50
47
|
app = null;
|
|
51
48
|
wss = null;
|
|
@@ -74,6 +71,7 @@ class DLServer {
|
|
|
74
71
|
dlOptions: {
|
|
75
72
|
debug: process.env.DS_DEBUG == '1',
|
|
76
73
|
saveDir: process.env.DS_SAVE_DIR || './downloads',
|
|
74
|
+
threadNum: 4,
|
|
77
75
|
},
|
|
78
76
|
};
|
|
79
77
|
/** 下载任务缓存 */
|
|
@@ -175,11 +173,10 @@ class DLServer {
|
|
|
175
173
|
const { default: express } = await Promise.resolve().then(() => __importStar(require('express')));
|
|
176
174
|
const { WebSocketServer } = await Promise.resolve().then(() => __importStar(require('ws')));
|
|
177
175
|
const app = (this.app = express());
|
|
178
|
-
const server = app.listen(this.options.port, () => utils_js_1.logger.info(`Server running on port ${this.options.port}`));
|
|
176
|
+
const server = app.listen(this.options.port, () => utils_js_1.logger.info(`Server running on port ${(0, console_log_colors_1.green)(this.options.port)}`));
|
|
179
177
|
const wss = (this.wss = new WebSocketServer({ server }));
|
|
180
178
|
app.use(express.json());
|
|
181
179
|
app.use(express.static((0, node_path_1.resolve)(__dirname, '../../client')));
|
|
182
|
-
// headers
|
|
183
180
|
app.use((req, res, next) => {
|
|
184
181
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
185
182
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
|
|
@@ -213,28 +210,15 @@ class DLServer {
|
|
|
213
210
|
}
|
|
214
211
|
ws.send(JSON.stringify({ type: 'serverInfo', data: this.serverInfo }));
|
|
215
212
|
ws.send(JSON.stringify({ type: 'tasks', data: Object.fromEntries(this.dlCacheClone()) }));
|
|
216
|
-
// ws.on('message', (message, _isBinary) => {
|
|
217
|
-
// logger.info('Received message from client:', (message as Buffer).toString('utf8'));
|
|
218
|
-
// });
|
|
219
|
-
});
|
|
220
|
-
wss.on('close', () => {
|
|
221
|
-
utils_js_1.logger.info('WebSocket server closed');
|
|
222
|
-
});
|
|
223
|
-
wss.on('error', err => {
|
|
224
|
-
utils_js_1.logger.error('WebSocket server error:', err);
|
|
225
|
-
});
|
|
226
|
-
wss.on('listening', () => {
|
|
227
|
-
utils_js_1.logger.info(`WebSocket server listening on port ${this.options.port}`);
|
|
228
213
|
});
|
|
214
|
+
wss.on('close', () => utils_js_1.logger.info('WebSocket server closed'));
|
|
215
|
+
wss.on('error', err => utils_js_1.logger.error('WebSocket server error:', err));
|
|
216
|
+
wss.on('listening', () => utils_js_1.logger.info(`WebSocket server listening on port ${(0, console_log_colors_1.green)(this.options.port)}`));
|
|
229
217
|
return { app, wss };
|
|
230
218
|
}
|
|
231
219
|
startDownload(url, options) {
|
|
232
220
|
const cacheItem = this.dlCache.get(url);
|
|
233
|
-
const dlOptions = {
|
|
234
|
-
...this.cfg.dlOptions,
|
|
235
|
-
...options,
|
|
236
|
-
cacheDir: this.options.cacheDir,
|
|
237
|
-
};
|
|
221
|
+
const dlOptions = (0, format_options_js_1.formatOptions)(url, { ...this.cfg.dlOptions, ...options, cacheDir: this.options.cacheDir })[1];
|
|
238
222
|
utils_js_1.logger.debug('startDownload', url, dlOptions, cacheItem?.status);
|
|
239
223
|
if (cacheItem && cacheItem?.status === 'resume')
|
|
240
224
|
return cacheItem.options;
|
|
@@ -242,12 +226,11 @@ class DLServer {
|
|
|
242
226
|
if (cacheItem)
|
|
243
227
|
cacheItem.status = 'pending';
|
|
244
228
|
else
|
|
245
|
-
this.dlCache.set(url, { options
|
|
229
|
+
this.dlCache.set(url, { options, dlOptions, status: 'pending', url });
|
|
246
230
|
return cacheItem?.options || dlOptions;
|
|
247
231
|
}
|
|
248
232
|
let workPoll = cacheItem?.workPoll;
|
|
249
|
-
const
|
|
250
|
-
const defaultItem = { options: dlOptions, status: 'resume', url };
|
|
233
|
+
const defaultItem = { options, dlOptions, status: 'resume', url };
|
|
251
234
|
const opts = {
|
|
252
235
|
...dlOptions,
|
|
253
236
|
showProgress: dlOptions.debug || this.options.debug,
|
|
@@ -259,20 +242,21 @@ class DLServer {
|
|
|
259
242
|
this.dlCache.set(url, item);
|
|
260
243
|
this.saveCache();
|
|
261
244
|
this.wsSend('progress', url);
|
|
245
|
+
return status !== 'pause';
|
|
262
246
|
},
|
|
263
247
|
};
|
|
264
|
-
const afterDownload = (
|
|
248
|
+
const afterDownload = (r, url) => {
|
|
265
249
|
const item = this.dlCache.get(url) || defaultItem;
|
|
266
|
-
if (filepath && (0, node_fs_1.existsSync)(filepath)) {
|
|
267
|
-
item.localVideo = filepath;
|
|
268
|
-
item.downloadedSize = (0, node_fs_1.statSync)(filepath).size;
|
|
250
|
+
if (r.filepath && (0, node_fs_1.existsSync)(r.filepath)) {
|
|
251
|
+
item.localVideo = r.filepath;
|
|
252
|
+
item.downloadedSize = (0, node_fs_1.statSync)(r.filepath).size;
|
|
269
253
|
}
|
|
270
|
-
else if (!errmsg && opts.convert !== false)
|
|
271
|
-
errmsg = '下载失败';
|
|
254
|
+
else if (!r.errmsg && opts.convert !== false)
|
|
255
|
+
r.errmsg = '下载失败';
|
|
272
256
|
item.endTime = Date.now();
|
|
273
|
-
item.
|
|
274
|
-
item.
|
|
275
|
-
utils_js_1.logger.info('Download complete:', item.status, (0, console_log_colors_1.red)(
|
|
257
|
+
item.errmsg = r.errmsg;
|
|
258
|
+
item.status = r.errmsg ? 'error' : 'done';
|
|
259
|
+
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));
|
|
276
260
|
this.dlCache.set(url, item);
|
|
277
261
|
this.wsSend('progress', url);
|
|
278
262
|
this.saveCache();
|
|
@@ -288,21 +272,19 @@ class DLServer {
|
|
|
288
272
|
if (cacheItem)
|
|
289
273
|
cacheItem.status = 'resume';
|
|
290
274
|
try {
|
|
291
|
-
if (
|
|
275
|
+
if (dlOptions.type === 'parser') {
|
|
292
276
|
const vp = new index_js_1.VideoParser();
|
|
293
|
-
vp.download(url, opts).then(r =>
|
|
294
|
-
|
|
295
|
-
|
|
277
|
+
vp.download(url, opts).then(r => afterDownload(r, url));
|
|
278
|
+
}
|
|
279
|
+
else if (dlOptions.type === 'file') {
|
|
280
|
+
(0, file_download_js_1.fileDownload)(url, opts).then(r => afterDownload(r, url));
|
|
296
281
|
}
|
|
297
282
|
else {
|
|
298
|
-
(0, m3u8_download_js_1.m3u8Download)(url, opts).then(r =>
|
|
299
|
-
const errmsg = 'error' in r ? r.error.cause || r.error.message : '';
|
|
300
|
-
afterDownload({ filepath: r.filepath, errmsg });
|
|
301
|
-
});
|
|
283
|
+
(0, m3u8_download_js_1.m3u8Download)(url, opts).then(r => afterDownload(r, url));
|
|
302
284
|
}
|
|
303
285
|
}
|
|
304
286
|
catch (error) {
|
|
305
|
-
afterDownload({ filepath: '', errmsg: error.message });
|
|
287
|
+
afterDownload({ filepath: '', errmsg: error.message }, url);
|
|
306
288
|
utils_js_1.logger.error('下载失败:', error);
|
|
307
289
|
}
|
|
308
290
|
return dlOptions;
|
|
@@ -315,8 +297,10 @@ class DLServer {
|
|
|
315
297
|
const item = this.dlCache.get(data);
|
|
316
298
|
if (item) {
|
|
317
299
|
const { workPoll, ...stats } = item;
|
|
318
|
-
data = { ...stats, url: data };
|
|
300
|
+
data = [{ ...stats, url: data }];
|
|
319
301
|
}
|
|
302
|
+
else
|
|
303
|
+
return;
|
|
320
304
|
}
|
|
321
305
|
// 广播进度信息给所有客户端
|
|
322
306
|
this.wss.clients.forEach(client => {
|
|
@@ -330,13 +314,11 @@ class DLServer {
|
|
|
330
314
|
app.get('/healthcheck', (_req, res) => {
|
|
331
315
|
res.json({ message: 'ok', code: 0 });
|
|
332
316
|
});
|
|
333
|
-
// API to set default config
|
|
334
317
|
app.post('/config', (req, res) => {
|
|
335
318
|
const config = req.body;
|
|
336
319
|
this.saveConfig(config);
|
|
337
320
|
res.json({ message: 'Config updated successfully', code: 0 });
|
|
338
321
|
});
|
|
339
|
-
// API to get default config
|
|
340
322
|
app.get('/config', (_req, res) => {
|
|
341
323
|
res.json({ ...this.cfg.dlOptions, ...this.cfg.webOptions });
|
|
342
324
|
});
|
|
@@ -405,9 +387,9 @@ class DLServer {
|
|
|
405
387
|
const urlsToPause = all ? [...this.dlCache.keys()] : urls;
|
|
406
388
|
const list = [];
|
|
407
389
|
for (const url of urlsToPause) {
|
|
408
|
-
const item = this.dlCache.get(url);
|
|
409
|
-
if (item?.status
|
|
410
|
-
(0, m3u8_download_js_1.m3u8DLStop)(url,
|
|
390
|
+
const { workPoll, ...item } = this.dlCache.get(url);
|
|
391
|
+
if (['resume', 'pending'].includes(item?.status)) {
|
|
392
|
+
(0, m3u8_download_js_1.m3u8DLStop)(url, workPoll);
|
|
411
393
|
item.status = item.tsSuccess === item.tsCount ? 'done' : 'pause';
|
|
412
394
|
list.push(item);
|
|
413
395
|
}
|
|
@@ -425,15 +407,15 @@ class DLServer {
|
|
|
425
407
|
const item = this.dlCache.get(url);
|
|
426
408
|
if (['pause', 'error'].includes(item?.status)) {
|
|
427
409
|
this.startDownload(url, item.options);
|
|
428
|
-
|
|
410
|
+
const { workPoll, ...t } = item;
|
|
411
|
+
list.push(t);
|
|
429
412
|
}
|
|
430
413
|
else
|
|
431
414
|
console.log(item?.status, url);
|
|
432
415
|
}
|
|
433
|
-
|
|
434
|
-
if (count)
|
|
416
|
+
if (list.length)
|
|
435
417
|
this.wsSend('progress', list);
|
|
436
|
-
res.json({ message:
|
|
418
|
+
res.json({ message: list.length ? `已恢复 ${list.length} 个下载任务` : '没有找到可恢复的下载任务', code: 0, count: list.length });
|
|
437
419
|
});
|
|
438
420
|
// API to delete download
|
|
439
421
|
app.post('/delete', (req, res) => {
|
|
@@ -445,6 +427,7 @@ class DLServer {
|
|
|
445
427
|
if (item) {
|
|
446
428
|
(0, m3u8_download_js_1.m3u8DLStop)(url, item.workPoll);
|
|
447
429
|
this.dlCache.delete(url);
|
|
430
|
+
list.push(item.url);
|
|
448
431
|
if (deleteCache && item.current?.tsOut) {
|
|
449
432
|
const cacheDir = (0, node_path_1.dirname)(item.current.tsOut);
|
|
450
433
|
if ((0, node_fs_1.existsSync)(cacheDir))
|
|
@@ -457,13 +440,11 @@ class DLServer {
|
|
|
457
440
|
(0, node_fs_1.unlinkSync)(filepath);
|
|
458
441
|
});
|
|
459
442
|
}
|
|
460
|
-
list.push(item);
|
|
461
443
|
}
|
|
462
444
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
res.json({ message: `已删除 ${count} 个下载任务`, code: 0, count });
|
|
445
|
+
if (list.length)
|
|
446
|
+
this.wsSend('delete', list);
|
|
447
|
+
res.json({ message: `已删除 ${list.length} 个下载任务`, code: 0, count: list.length });
|
|
467
448
|
});
|
|
468
449
|
app.get(/^\/localplay\/(.*)$/, (req, res) => {
|
|
469
450
|
let filepath = decodeURIComponent(req.params[0]);
|
package/cjs/types/m3u8.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyObject } from '@lzwme/fe-utils';
|
|
1
|
+
import type { AnyObject, DownloadResult } from '@lzwme/fe-utils';
|
|
2
2
|
import type { IncomingHttpHeaders } from 'node:http';
|
|
3
3
|
import type { WorkerPool } from '../lib/worker_pool';
|
|
4
4
|
export interface TsItemInfo {
|
|
@@ -100,12 +100,9 @@ export interface M3u8DLOptions {
|
|
|
100
100
|
/** 当初始化完成、下载开始时回调 */
|
|
101
101
|
onInited?: (stats: M3u8DLProgressStats, m3u8Info: M3u8Info, workPoll: M3u8WorkerPool) => void;
|
|
102
102
|
/** 每当 ts 文件下载完成时回调,可用于自定义进度控制 */
|
|
103
|
-
onProgress?: (finished: number, total: number, currentInfo: TsItemInfo, stats: M3u8DLProgressStats) => void;
|
|
103
|
+
onProgress?: (finished: number, total: number, currentInfo: TsItemInfo, stats: M3u8DLProgressStats) => void | boolean;
|
|
104
104
|
/** 下载完成时回调,主要用于内部多任务管理 */
|
|
105
|
-
onComplete?: (result:
|
|
106
|
-
error?: Error;
|
|
107
|
-
filepath?: string;
|
|
108
|
-
}) => void;
|
|
105
|
+
onComplete?: (result: M3u8DLResult) => void;
|
|
109
106
|
/** 并发下载线程数。取决于服务器限制,过多可能会容易下载失败。一般建议不超过 8 个。默认为 cpu数 * 2,但不超过 8 */
|
|
110
107
|
threadNum?: number;
|
|
111
108
|
/** 最大并发下载任务数 */
|
|
@@ -128,6 +125,21 @@ export interface M3u8DLOptions {
|
|
|
128
125
|
play?: boolean;
|
|
129
126
|
/** 下载完毕后,是否合并转换为 mp4 或 ts 文件。默认为 true */
|
|
130
127
|
convert?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* 下载类型。默认自动识别
|
|
130
|
+
* - 'm3u8':下载 m3u8 文件
|
|
131
|
+
* - 'file':下载普通文件
|
|
132
|
+
* - 'parser':下载 VideoParser 支持解析的平台视频文件
|
|
133
|
+
*/
|
|
134
|
+
type?: 'm3u8' | 'file' | 'parser';
|
|
135
|
+
}
|
|
136
|
+
export interface M3u8DLResult extends Partial<DownloadResult> {
|
|
137
|
+
/** 下载进度统计 */
|
|
138
|
+
stats?: M3u8DLProgressStats;
|
|
139
|
+
/** 下载选项 */
|
|
140
|
+
options?: M3u8DLOptions;
|
|
141
|
+
/** m3u8 文件信息 */
|
|
142
|
+
m3u8Info?: M3u8Info;
|
|
131
143
|
}
|
|
132
144
|
export interface WorkerTaskInfo {
|
|
133
145
|
/** m3u8 文件地址 */
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export interface VideoInfo {
|
|
2
|
-
author: string;
|
|
3
|
-
uid: string;
|
|
4
|
-
avatar: string;
|
|
5
|
-
like: number;
|
|
6
|
-
time: number;
|
|
7
|
-
title: string;
|
|
8
|
-
cover: string;
|
|
9
2
|
url: string;
|
|
3
|
+
title: string;
|
|
4
|
+
author?: string;
|
|
5
|
+
avatar?: string;
|
|
6
|
+
time?: number;
|
|
7
|
+
cover?: string;
|
|
8
|
+
uid?: string;
|
|
9
|
+
like?: number;
|
|
10
10
|
/** 来源页面 */
|
|
11
11
|
referer?: string;
|
|
12
12
|
music?: {
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { ApiResponse, M3u8DLOptions, M3u8DLProgressStats, VideoInfo } from '../types';
|
|
1
|
+
import type { ApiResponse, M3u8DLOptions, M3u8DLResult, VideoInfo } from '../types';
|
|
3
2
|
export declare class VideoParser {
|
|
4
3
|
private static readonly platforms;
|
|
5
4
|
/**
|
|
6
5
|
* 解析视频 URL
|
|
7
6
|
*/
|
|
8
7
|
parse(url: string, headers?: Record<string, string>): Promise<ApiResponse<VideoInfo>>;
|
|
9
|
-
download(url: string, options: M3u8DLOptions): Promise<
|
|
10
|
-
stats?: M3u8DLProgressStats;
|
|
11
|
-
}>;
|
|
8
|
+
download(url: string, options: M3u8DLOptions): Promise<M3u8DLResult>;
|
|
12
9
|
/**
|
|
13
10
|
* 根据 URL 获取平台标识
|
|
14
11
|
*/
|
|
@@ -3,12 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.VideoParser = void 0;
|
|
4
4
|
const url_1 = require("url");
|
|
5
5
|
const node_path_1 = require("node:path");
|
|
6
|
-
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
7
|
-
const console_log_colors_1 = require("console-log-colors");
|
|
8
6
|
const douyin_parser_1 = require("./parsers/douyin-parser");
|
|
9
7
|
const pipixia_parser_1 = require("./parsers/pipixia-parser");
|
|
10
8
|
const weibo_parser_1 = require("./parsers/weibo-parser");
|
|
9
|
+
const base_parser_1 = require("./parsers/base-parser");
|
|
11
10
|
const utils_1 = require("../lib/utils");
|
|
11
|
+
const file_download_1 = require("../lib/file-download");
|
|
12
12
|
class VideoParser {
|
|
13
13
|
static platforms = {
|
|
14
14
|
pipixia: {
|
|
@@ -23,6 +23,10 @@ class VideoParser {
|
|
|
23
23
|
class: weibo_parser_1.WeiboParser,
|
|
24
24
|
domains: ['weibo.com'],
|
|
25
25
|
},
|
|
26
|
+
unknown: {
|
|
27
|
+
class: base_parser_1.BaseParser,
|
|
28
|
+
domains: ['**'],
|
|
29
|
+
},
|
|
26
30
|
};
|
|
27
31
|
/**
|
|
28
32
|
* 解析视频 URL
|
|
@@ -38,83 +42,19 @@ class VideoParser {
|
|
|
38
42
|
const info = await this.parse(url);
|
|
39
43
|
utils_1.logger.debug('解析视频信息', info);
|
|
40
44
|
if (info.code || !info.data?.url)
|
|
41
|
-
return {
|
|
45
|
+
return { errmsg: info.message || '解析视频信息失败', options };
|
|
42
46
|
if (!options.filename && info.data.title) {
|
|
43
47
|
options.filename = info.data.title.replaceAll(/[\s\\/:*?"<>|]/g, '_');
|
|
44
48
|
}
|
|
45
|
-
|
|
49
|
+
if (!options.type)
|
|
50
|
+
options.type = 'parser';
|
|
46
51
|
if (!(0, node_path_1.extname)(options.filename))
|
|
47
52
|
options.filename += '.mp4';
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
startTime,
|
|
52
|
-
progress: 0,
|
|
53
|
-
tsSuccess: 0,
|
|
54
|
-
tsFailed: 0,
|
|
55
|
-
tsCount: 0,
|
|
56
|
-
duration: 0,
|
|
57
|
-
durationDownloaded: 0,
|
|
58
|
-
downloadedSize: 0,
|
|
59
|
-
avgSpeed: 0,
|
|
60
|
-
avgSpeedDesc: '0B/s',
|
|
61
|
-
speed: 0,
|
|
62
|
-
speedDesc: '0B/s',
|
|
63
|
-
remainingTime: 0,
|
|
64
|
-
filename: options.filename,
|
|
65
|
-
localVideo: (0, node_path_1.resolve)(options.saveDir, options.filename),
|
|
53
|
+
options.headers = {
|
|
54
|
+
referer: info.data.referer || info.data.url,
|
|
55
|
+
...(0, utils_1.formatHeaders)(options.headers),
|
|
66
56
|
};
|
|
67
|
-
|
|
68
|
-
if (options.onInited)
|
|
69
|
-
options.onInited(stats, null, null);
|
|
70
|
-
try {
|
|
71
|
-
const r = await (0, fe_utils_1.download)({
|
|
72
|
-
url,
|
|
73
|
-
filepath: stats.localVideo,
|
|
74
|
-
paralelism: options.threadNum,
|
|
75
|
-
force: options.force,
|
|
76
|
-
requestOptions: {
|
|
77
|
-
headers: {
|
|
78
|
-
referer: info.data.referer || info.data.url,
|
|
79
|
-
...(0, utils_1.formatHeaders)(options.headers),
|
|
80
|
-
},
|
|
81
|
-
rejectUnauthorized: false,
|
|
82
|
-
},
|
|
83
|
-
onProgress: info => {
|
|
84
|
-
stats.progress = +info.percent.toFixed(2);
|
|
85
|
-
stats.size = info.size;
|
|
86
|
-
stats.downloadedSize = info.downloaded;
|
|
87
|
-
stats.speed = info.speed;
|
|
88
|
-
stats.speedDesc = (0, fe_utils_1.formatByteSize)(stats.speed) + '/s';
|
|
89
|
-
stats.remainingTime = Math.round((info.size - info.downloaded) / stats.speed);
|
|
90
|
-
if (options.showProgress) {
|
|
91
|
-
const processBar = info.percent === -1 ? '' : '='.repeat(Math.floor(stats.progress * 0.2)).padEnd(20, '-');
|
|
92
|
-
utils_1.logger.logInline(`${stats.progress}% [${(0, console_log_colors_1.greenBright)(processBar)}] ` +
|
|
93
|
-
`${(0, console_log_colors_1.blueBright)((0, fe_utils_1.formatByteSize)(stats.downloadedSize))} ${(0, console_log_colors_1.yellowBright)((0, fe_utils_1.formatTimeCost)(startTime))} ${(0, console_log_colors_1.magentaBright)(stats.speedDesc)} ` +
|
|
94
|
-
(stats.progress === 100 ? '\n' : stats.remainingTime ? `${(0, console_log_colors_1.cyan)((0, fe_utils_1.formatTimeCost)(Date.now() - stats.remainingTime))}` : ''));
|
|
95
|
-
}
|
|
96
|
-
if (options.onProgress) {
|
|
97
|
-
options.onProgress(info.downloaded, info.size, null, stats);
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
stats.endTime = Date.now();
|
|
102
|
-
return {
|
|
103
|
-
code: r.filepath ? 0 : 201,
|
|
104
|
-
message: r.filepath ? '下载完成' : '下载失败',
|
|
105
|
-
data: r,
|
|
106
|
-
stats,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
catch (error) {
|
|
110
|
-
utils_1.logger.error('下载失败', error.message, (0, console_log_colors_1.gray)(url));
|
|
111
|
-
stats.errmsg = error.message;
|
|
112
|
-
return {
|
|
113
|
-
code: 201,
|
|
114
|
-
message: '下载失败: ' + error.message,
|
|
115
|
-
stats,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
57
|
+
return (0, file_download_1.fileDownload)(url, options);
|
|
118
58
|
}
|
|
119
59
|
/**
|
|
120
60
|
* 根据 URL 获取平台标识
|
|
@@ -129,12 +69,12 @@ class VideoParser {
|
|
|
129
69
|
return { url, platform };
|
|
130
70
|
}
|
|
131
71
|
}
|
|
72
|
+
return { url, platform: 'unknown' };
|
|
132
73
|
}
|
|
133
74
|
catch (error) {
|
|
134
75
|
console.error('解析 URL 失败', url, error);
|
|
135
76
|
return null;
|
|
136
77
|
}
|
|
137
|
-
return null;
|
|
138
78
|
}
|
|
139
79
|
/**
|
|
140
80
|
* 获取所有支持的平台列表
|
|
@@ -2,5 +2,5 @@ import type { ApiResponse, VideoInfo } from '../../types';
|
|
|
2
2
|
export declare abstract class BaseParser {
|
|
3
3
|
protected static success<T>(data: T): ApiResponse<T>;
|
|
4
4
|
protected static error(code: number, message: string): ApiResponse<null>;
|
|
5
|
-
static parse(
|
|
5
|
+
static parse(url: string, headers?: Record<string, string>): Promise<ApiResponse<VideoInfo>>;
|
|
6
6
|
}
|
|
@@ -3,21 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.BaseParser = void 0;
|
|
4
4
|
class BaseParser {
|
|
5
5
|
static success(data) {
|
|
6
|
-
return {
|
|
7
|
-
code: 0,
|
|
8
|
-
message: 'success',
|
|
9
|
-
data,
|
|
10
|
-
};
|
|
6
|
+
return { code: 0, message: 'success', data };
|
|
11
7
|
}
|
|
12
8
|
static error(code, message) {
|
|
13
|
-
return {
|
|
14
|
-
code,
|
|
15
|
-
message,
|
|
16
|
-
data: null,
|
|
17
|
-
};
|
|
9
|
+
return { code, message, data: null };
|
|
18
10
|
}
|
|
19
|
-
static parse(
|
|
20
|
-
return Promise.resolve(this.success(
|
|
11
|
+
static parse(url, headers = {}) {
|
|
12
|
+
return Promise.resolve(this.success({ url, title: '', referer: headers.referer }));
|
|
21
13
|
}
|
|
22
14
|
}
|
|
23
15
|
exports.BaseParser = BaseParser;
|
package/client/index.html
CHANGED
|
@@ -426,7 +426,7 @@ services:
|
|
|
426
426
|
<div class="flex items-center">
|
|
427
427
|
<input type="checkbox" :checked="selectedTasks.includes(task.url)" @change="toggleTaskSelection(task.url)"
|
|
428
428
|
class="form-checkbox h-5 w-5 text-blue-500 rounded focus:ring-blue-500 mr-2"
|
|
429
|
-
|
|
429
|
+
title="'选择任务:' + (task.localVideo || task.filename || task.url)">
|
|
430
430
|
<h3 class="font-bold text-green-600 truncate" :title="task.url">
|
|
431
431
|
{{ task.localVideo || task.filename || task.url }}
|
|
432
432
|
</h3>
|
|
@@ -601,7 +601,7 @@ services:
|
|
|
601
601
|
};
|
|
602
602
|
|
|
603
603
|
Vue.prototype.T = T;
|
|
604
|
-
new Vue({
|
|
604
|
+
window.APP = new Vue({
|
|
605
605
|
el: '#app',
|
|
606
606
|
data: {
|
|
607
607
|
ws: null,
|
|
@@ -691,9 +691,12 @@ services:
|
|
|
691
691
|
forceUpdate: function () {
|
|
692
692
|
const now = Date.now();
|
|
693
693
|
if (now - this.forceUpdateTime > 500) {
|
|
694
|
-
this.$forceUpdate();
|
|
695
694
|
this.forceUpdateTime = now;
|
|
696
|
-
|
|
695
|
+
this.$forceUpdate();
|
|
696
|
+
} else {
|
|
697
|
+
if (this.forceUpdateTimeout) clearTimeout(this.forceUpdateTimeout);
|
|
698
|
+
this.forceUpdateTimeout = setTimeout(() => this.forceUpdate(), 500);
|
|
699
|
+
}
|
|
697
700
|
},
|
|
698
701
|
wsConnect: function (reconnectDelay = 3000) {
|
|
699
702
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
@@ -716,9 +719,15 @@ services:
|
|
|
716
719
|
if (!Array.isArray(data)) data = [data];
|
|
717
720
|
this.$nextTick(() => {
|
|
718
721
|
data.forEach(item => item.url && (this.tasks[item.url] = item));
|
|
719
|
-
|
|
722
|
+
this.forceUpdate();
|
|
720
723
|
});
|
|
721
724
|
break;
|
|
725
|
+
case 'delete':
|
|
726
|
+
if (Array.isArray(data)) {
|
|
727
|
+
data.forEach(url => delete this.tasks[url]);
|
|
728
|
+
this.forceUpdate();
|
|
729
|
+
}
|
|
730
|
+
break;
|
|
722
731
|
case 'queueStatus':
|
|
723
732
|
this.queueStatus = data;
|
|
724
733
|
break;
|
|
@@ -928,7 +937,7 @@ services:
|
|
|
928
937
|
},
|
|
929
938
|
/** 边下边播 */
|
|
930
939
|
localPlay: function (task) {
|
|
931
|
-
const url = location.origin + '/localplay/' +
|
|
940
|
+
const url = location.origin + '/localplay/' + (task.localVideo || task.localM3u8);
|
|
932
941
|
console.log(task);
|
|
933
942
|
// return window.open(`./play.html?url=${encodeURIComponent(url)}`);
|
|
934
943
|
Swal.fire({
|