@lzwme/m3u8-dl 0.0.2 → 0.0.4
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 +22 -12
- package/cjs/cli.js +123 -8
- package/cjs/lib/local-play.d.ts +1 -1
- package/cjs/lib/local-play.js +1 -0
- package/cjs/lib/m3u8-convert.d.ts +1 -1
- package/cjs/lib/m3u8-convert.js +9 -6
- package/cjs/lib/m3u8-download.d.ts +8 -2
- package/cjs/lib/m3u8-download.js +61 -22
- package/cjs/lib/parseM3u8.d.ts +1 -1
- package/cjs/lib/parseM3u8.js +3 -1
- package/cjs/lib/storage.d.ts +7 -0
- package/cjs/lib/storage.js +7 -0
- package/cjs/lib/ts-download.d.ts +1 -1
- package/cjs/lib/ts-download.js +3 -0
- package/cjs/lib/utils.js +2 -1
- package/cjs/lib/video-search.d.ts +14 -0
- package/cjs/lib/video-search.js +90 -0
- package/cjs/lib/worker_pool.d.ts +3 -0
- package/cjs/lib/worker_pool.js +12 -1
- package/cjs/m3u8-batch-download.d.ts +1 -1
- package/cjs/m3u8-batch-download.js +36 -5
- package/cjs/types/index.d.ts +2 -0
- package/cjs/types/index.js +18 -0
- package/cjs/{type.d.ts → types/m3u8.d.ts} +1 -1
- package/cjs/types/m3u8.js +4 -0
- package/cjs/types/video-search.d.ts +122 -0
- package/cjs/types/video-search.js +2 -0
- package/package.json +14 -13
package/README.MD
CHANGED
|
@@ -12,16 +12,19 @@
|
|
|
12
12
|
[![GitHub forks][forks-badge]][forks-url]
|
|
13
13
|
[![GitHub stars][stars-badge]][stars-url]
|
|
14
14
|
|
|
15
|
-
一个 m3u8
|
|
15
|
+
一个 m3u8 文件视频批量下载工具。
|
|
16
|
+
|
|
17
|
+

|
|
16
18
|
|
|
17
19
|
## 功能特性(Features)
|
|
18
20
|
|
|
19
21
|
- 多线程下载。线程池模式的多线程下载。
|
|
20
22
|
- `边下边播模式`。支持使用已下载的 ts 缓存文件在线播放。
|
|
21
23
|
- 支持指定多个 m3u8 地址批量下载。
|
|
22
|
-
-
|
|
23
|
-
- 支持常见的 AES
|
|
24
|
-
-
|
|
24
|
+
- 支持缓存续传。下载失败会保留缓存,重试时只下载失败的片段。
|
|
25
|
+
- 支持常见的 AES 加密视频流解密。
|
|
26
|
+
- 自动转换为 mp4。**需全局安装 ffmpeg**
|
|
27
|
+
- `[NEW!]`支持指定采集站标准 API,以命令行交互的方式搜索和下载。
|
|
25
28
|
|
|
26
29
|
## 安装(Install)
|
|
27
30
|
|
|
@@ -36,7 +39,7 @@ m3u8dl -h
|
|
|
36
39
|
npx @lzwme/m3u8-dl -h
|
|
37
40
|
```
|
|
38
41
|
|
|
39
|
-
##
|
|
42
|
+
## Useage
|
|
40
43
|
|
|
41
44
|
提示:如需要下载并转换为 `mp4` 视频格式,您需全局安装 [ffmpeg](https://ffmpeg.org/download.html)。
|
|
42
45
|
|
|
@@ -46,20 +49,20 @@ npx @lzwme/m3u8-dl -h
|
|
|
46
49
|
m3u8dl --help
|
|
47
50
|
```
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
#### 下载指定 URL 的 m3u8 文件:
|
|
50
53
|
|
|
51
54
|
```bash
|
|
52
55
|
m3u8dl https://lzw.me/x/m3u8-player/test.m3u8
|
|
53
56
|
```
|
|
54
57
|
|
|
55
|
-
|
|
58
|
+
#### 批量下载示例一:
|
|
56
59
|
|
|
57
60
|
```bash
|
|
58
61
|
# 下载多个文件:
|
|
59
62
|
m3u8dl "第1集|https://s.xlzys.com/play/zbqMZYRb/index.m3u8" "第2集|https://s.xlzys.com/play/PdyJXrwe/index.m3u8" --filename "三体"
|
|
60
63
|
```
|
|
61
64
|
|
|
62
|
-
|
|
65
|
+
#### 批量下载示例二:
|
|
63
66
|
|
|
64
67
|
新建文件 `三体.txt`,内容格式:
|
|
65
68
|
|
|
@@ -77,6 +80,17 @@ m3u8dl 三体.txt
|
|
|
77
80
|
|
|
78
81
|
提示:可创建并指定多个 txt 文件实现对多个影视剧集的一键批量下载。
|
|
79
82
|
|
|
83
|
+
#### 指定采集站 API 搜索并下载
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
m3u8dl search -h
|
|
87
|
+
|
|
88
|
+
# 指定采集站 API url 地址(会缓存),然后按提示操作
|
|
89
|
+
m3u8dl s -u https://jyzyapi.com/provide/vod/
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**声明:** 以上仅作示例,请自行搜索查找可用的采集站 API。本工具仅用作技术研究学习,不提供任何具体资源类信息。
|
|
93
|
+
|
|
80
94
|
### API 调用
|
|
81
95
|
|
|
82
96
|
```ts
|
|
@@ -93,10 +107,6 @@ for (const filepath of fileList) {
|
|
|
93
107
|
}
|
|
94
108
|
```
|
|
95
109
|
|
|
96
|
-
## API 文档
|
|
97
|
-
|
|
98
|
-
- [https://lzwme.github.io/m3u8-dl/](https://lzwme.github.io/m3u8-dl/)
|
|
99
|
-
|
|
100
110
|
## 开发(Development)
|
|
101
111
|
|
|
102
112
|
本地二次开发:
|
package/cjs/cli.js
CHANGED
|
@@ -4,9 +4,19 @@ const node_path_1 = require("node:path");
|
|
|
4
4
|
const commander_1 = require("commander");
|
|
5
5
|
const console_log_colors_1 = require("console-log-colors");
|
|
6
6
|
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
7
|
+
const enquirer_1 = require("enquirer");
|
|
7
8
|
const utils_js_1 = require("./lib/utils.js");
|
|
8
9
|
const m3u8_batch_download_1 = require("./m3u8-batch-download");
|
|
10
|
+
const video_search_js_1 = require("./lib/video-search.js");
|
|
9
11
|
const pkg = (0, fe_utils_1.readJsonFileSync)((0, node_path_1.resolve)(__dirname, '../package.json'));
|
|
12
|
+
process.on('unhandledRejection', (r, p) => {
|
|
13
|
+
console.log('[退出]UnhandledPromiseRejection', r, p);
|
|
14
|
+
process.exit();
|
|
15
|
+
});
|
|
16
|
+
process.on('SIGINT', signal => {
|
|
17
|
+
utils_js_1.logger.info('强制退出', signal);
|
|
18
|
+
process.exit();
|
|
19
|
+
});
|
|
10
20
|
commander_1.program
|
|
11
21
|
.version(pkg.version, '-v, --version')
|
|
12
22
|
.description((0, console_log_colors_1.cyanBright)(pkg.description))
|
|
@@ -21,14 +31,8 @@ commander_1.program
|
|
|
21
31
|
.option('-C, --cache-dir <dirpath>', `临时文件保存目录。默认为 cache`)
|
|
22
32
|
.option('-S, --save-dir <dirpath>', `下载文件保存的路径。默认为当前目录`)
|
|
23
33
|
.option('--no-del-cache', `下载成功后是否删除临时文件。默认为 true。保存临时文件可以在重复下载时识别缓存`, true)
|
|
24
|
-
.action(async (urls
|
|
25
|
-
|
|
26
|
-
utils_js_1.logger.updateOptions({ levelType: 'debug' });
|
|
27
|
-
}
|
|
28
|
-
else if (options.silent) {
|
|
29
|
-
utils_js_1.logger.updateOptions({ levelType: 'silent' });
|
|
30
|
-
options.progress = false;
|
|
31
|
-
}
|
|
34
|
+
.action(async (urls) => {
|
|
35
|
+
const options = getOptions();
|
|
32
36
|
utils_js_1.logger.debug(urls, options);
|
|
33
37
|
if (options.progress != null)
|
|
34
38
|
options.showProgress = options.progress;
|
|
@@ -38,4 +42,115 @@ commander_1.program
|
|
|
38
42
|
else
|
|
39
43
|
commander_1.program.help();
|
|
40
44
|
});
|
|
45
|
+
commander_1.program
|
|
46
|
+
.command('search [keyword]')
|
|
47
|
+
.alias('s')
|
|
48
|
+
.option('-u,--url <api...>', '影视搜索的接口地址(m3u8采集站标准接口)')
|
|
49
|
+
.description('m3u8视频在线搜索与下载')
|
|
50
|
+
.action(async (keyword, options) => {
|
|
51
|
+
VideoSerachAndDL(keyword, options, getOptions());
|
|
52
|
+
});
|
|
41
53
|
commander_1.program.parse(process.argv);
|
|
54
|
+
function getOptions() {
|
|
55
|
+
const options = commander_1.program.opts();
|
|
56
|
+
if (options.debug) {
|
|
57
|
+
utils_js_1.logger.updateOptions({ levelType: 'debug' });
|
|
58
|
+
}
|
|
59
|
+
else if (options.silent) {
|
|
60
|
+
utils_js_1.logger.updateOptions({ levelType: 'silent' });
|
|
61
|
+
options.progress = false;
|
|
62
|
+
}
|
|
63
|
+
return options;
|
|
64
|
+
}
|
|
65
|
+
async function VideoSerachAndDL(keyword, options, baseOpts) {
|
|
66
|
+
const vs = new video_search_js_1.VideoSearch();
|
|
67
|
+
await vs.updateOptions({ api: options.url || [] });
|
|
68
|
+
let apiUrl = vs.api[0];
|
|
69
|
+
if (!options.url && vs.api.length > 0) {
|
|
70
|
+
await (0, enquirer_1.prompt)({
|
|
71
|
+
type: 'select',
|
|
72
|
+
name: 'k',
|
|
73
|
+
message: '请选择 API 站点',
|
|
74
|
+
choices: vs.api.map(d => ({ name: d, message: d })),
|
|
75
|
+
validate: value => value.length >= 1,
|
|
76
|
+
}).then(v => (apiUrl = v.k));
|
|
77
|
+
}
|
|
78
|
+
await (0, enquirer_1.prompt)({
|
|
79
|
+
type: 'input',
|
|
80
|
+
name: 'k',
|
|
81
|
+
message: '请输入关键字',
|
|
82
|
+
validate: value => value.length > 1,
|
|
83
|
+
initial: keyword,
|
|
84
|
+
}).then(v => (keyword = v.k));
|
|
85
|
+
const sRes = await vs.search(keyword, apiUrl);
|
|
86
|
+
utils_js_1.logger.debug(sRes);
|
|
87
|
+
if (!sRes.total) {
|
|
88
|
+
console.log(console_log_colors_1.color.green(`[${keyword}]`), `没有搜到结果`);
|
|
89
|
+
return VideoSerachAndDL(keyword, options, baseOpts);
|
|
90
|
+
}
|
|
91
|
+
const choices = sRes.list.map((d, idx) => ({
|
|
92
|
+
name: d.vod_id,
|
|
93
|
+
message: `${idx + 1}. [${d.type_name}] ${d.vod_name}`,
|
|
94
|
+
hint: `${d.vod_remarks}(${d.vod_time})`,
|
|
95
|
+
}));
|
|
96
|
+
const answer1 = await (0, enquirer_1.prompt)({
|
|
97
|
+
type: 'select',
|
|
98
|
+
name: 'vid',
|
|
99
|
+
pointer: '👉',
|
|
100
|
+
message: `查找到了 ${console_log_colors_1.color.greenBright(sRes.list.length)} 条结果,请选择:`,
|
|
101
|
+
choices: choices.concat({ name: -1, message: (0, console_log_colors_1.greenBright)('重新搜索'), hint: '' }),
|
|
102
|
+
});
|
|
103
|
+
if (answer1.vid === -1)
|
|
104
|
+
return VideoSerachAndDL(keyword, options, baseOpts);
|
|
105
|
+
const vResult = await vs.getVideoList(answer1.vid, apiUrl);
|
|
106
|
+
if (!vResult.list?.length) {
|
|
107
|
+
utils_js_1.logger.error('获取视频信息失败!', vResult.msg);
|
|
108
|
+
return VideoSerachAndDL(keyword, options, baseOpts);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const info = vResult.list[0];
|
|
112
|
+
const urls = info.vod_play_url
|
|
113
|
+
.split(info.vod_play_note)
|
|
114
|
+
.find(d => d.includes('.m3u8'))
|
|
115
|
+
.split('#');
|
|
116
|
+
utils_js_1.logger.debug(info, urls);
|
|
117
|
+
const r = (key, desc) => (info[key] ? ` [${desc}] ${(0, console_log_colors_1.greenBright)(info[key])}` : '');
|
|
118
|
+
console.log([
|
|
119
|
+
`\n [名称] ${(0, console_log_colors_1.cyanBright)(info.vod_name)}`,
|
|
120
|
+
r('vod_sub', '别名'),
|
|
121
|
+
` [更新] ${(0, console_log_colors_1.greenBright)(info.vod_remarks)}(${(0, console_log_colors_1.gray)(info.vod_time)})`,
|
|
122
|
+
r('vod_total', '总集数'),
|
|
123
|
+
r('type_name', '分类'),
|
|
124
|
+
r('vod_class', '类别'),
|
|
125
|
+
r('vod_writer', '作者'),
|
|
126
|
+
r('vod_area', '地区'),
|
|
127
|
+
r('vod_lang', '语言'),
|
|
128
|
+
r('vod_year', '年份'),
|
|
129
|
+
r('vod_douban_score', '评分'),
|
|
130
|
+
r('vod_pubdate', '上映日期'),
|
|
131
|
+
`\n${(0, console_log_colors_1.green)((info.vod_content || info.vod_blurb).replace(/<\/?.+?>/g, ''))}\n`, // 描述
|
|
132
|
+
]
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
.join('\n'), '\n');
|
|
135
|
+
const answer = await (0, enquirer_1.prompt)({
|
|
136
|
+
type: 'select',
|
|
137
|
+
name: 'url',
|
|
138
|
+
choices: [
|
|
139
|
+
{ name: '1', message: (0, console_log_colors_1.green)('全部下载') },
|
|
140
|
+
{ name: '-1', message: (0, console_log_colors_1.cyanBright)('重新搜索') },
|
|
141
|
+
].concat(urls.map((d, i) => ({ name: d, message: `${i + 1}. ${d}` }))),
|
|
142
|
+
message: `获取到了 ${console_log_colors_1.color.magentaBright(urls.length)} 条视频下载地址,请选择:`,
|
|
143
|
+
});
|
|
144
|
+
if (answer.url !== '-1') {
|
|
145
|
+
const p = await (0, enquirer_1.prompt)({
|
|
146
|
+
type: 'confirm',
|
|
147
|
+
name: 'play',
|
|
148
|
+
initial: baseOpts.play,
|
|
149
|
+
message: `【${(0, console_log_colors_1.greenBright)(info.vod_name)}】是否边下边播?`,
|
|
150
|
+
});
|
|
151
|
+
baseOpts.play = p.play;
|
|
152
|
+
await (0, m3u8_batch_download_1.m3u8BatchDownload)(answer.url === '1' ? urls : [answer.url], { filename: info.vod_name.replaceAll(' ', '_'), ...baseOpts });
|
|
153
|
+
}
|
|
154
|
+
return VideoSerachAndDL(keyword, options, baseOpts);
|
|
155
|
+
}
|
|
156
|
+
}
|
package/cjs/lib/local-play.d.ts
CHANGED
package/cjs/lib/local-play.js
CHANGED
|
@@ -68,6 +68,7 @@ async function createLocalServer(baseDir) {
|
|
|
68
68
|
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
69
69
|
res.end('Not found');
|
|
70
70
|
}).listen(port, () => {
|
|
71
|
+
console.log();
|
|
71
72
|
utils_1.logger.info('Created Local Server:', console_log_colors_1.color.greenBright(origin));
|
|
72
73
|
});
|
|
73
74
|
return { port, origin, server };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { M3u8DLOptions, TsItemInfo } from '../
|
|
1
|
+
import type { M3u8DLOptions, TsItemInfo } from '../types/m3u8';
|
|
2
2
|
export declare function m3u8Convert(options: M3u8DLOptions, data: TsItemInfo[]): Promise<string>;
|
package/cjs/lib/m3u8-convert.js
CHANGED
|
@@ -11,13 +11,15 @@ async function m3u8Convert(options, data) {
|
|
|
11
11
|
let ffmpegSupport = (0, utils_1.isSupportFfmpeg)();
|
|
12
12
|
let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename);
|
|
13
13
|
if (!ffmpegSupport)
|
|
14
|
-
filepath = filepath.replace(
|
|
14
|
+
filepath = filepath.replace(/\.mp4$/, '.ts');
|
|
15
15
|
if (!options.force && (0, node_fs_1.existsSync)(filepath))
|
|
16
16
|
return filepath;
|
|
17
17
|
utils_1.logger.info(`Starting ${ffmpegSupport ? 'convert to mp4' : 'merge into ts'} file:`, (0, console_log_colors_1.greenBright)(filepath));
|
|
18
18
|
if (ffmpegSupport) {
|
|
19
19
|
const inputFilePath = (0, node_path_1.resolve)(options.cacheDir, 'input.txt');
|
|
20
|
-
|
|
20
|
+
let filesAllArr = data.map(d => (0, node_path_1.resolve)(d.tsOut)).filter(d => (0, node_fs_1.existsSync)(d));
|
|
21
|
+
if (process.platform === 'win32')
|
|
22
|
+
filesAllArr = filesAllArr.map(d => d.replaceAll('\\', '/'));
|
|
21
23
|
await node_fs_1.promises.writeFile(inputFilePath, 'ffconcat version 1.0\nfile ' + filesAllArr.join('\nfile '));
|
|
22
24
|
const cmd = `ffmpeg -y -f concat -safe 0 -i ${inputFilePath} -acodec copy -vcodec copy -absf aac_adtstoasc ${filepath}`;
|
|
23
25
|
utils_1.logger.debug('[convert to mp4]cmd:', (0, console_log_colors_1.cyan)(cmd));
|
|
@@ -26,12 +28,13 @@ async function m3u8Convert(options, data) {
|
|
|
26
28
|
if (r.error)
|
|
27
29
|
utils_1.logger.error('Conversion to mp4 failed. Please confirm that `ffmpeg` is installed!', r.stderr);
|
|
28
30
|
}
|
|
29
|
-
|
|
31
|
+
if (!ffmpegSupport) {
|
|
32
|
+
filepath = filepath.replace(/\.mp4$/, '.ts');
|
|
30
33
|
await node_fs_1.promises.writeFile(filepath, Buffer.concat(data.map(d => (0, node_fs_1.readFileSync)(d.tsOut))));
|
|
31
34
|
}
|
|
32
|
-
if ((0, node_fs_1.existsSync)(filepath))
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
+
if (!(0, node_fs_1.existsSync)(filepath))
|
|
36
|
+
return '';
|
|
37
|
+
utils_1.logger.info(`File saved[${(0, console_log_colors_1.magentaBright)((0, helper_1.formatByteSize)((0, node_fs_1.statSync)(filepath).size))}]:`, (0, console_log_colors_1.greenBright)(filepath));
|
|
35
38
|
return filepath;
|
|
36
39
|
}
|
|
37
40
|
exports.m3u8Convert = m3u8Convert;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { WorkerPool } from './worker_pool';
|
|
2
|
+
import type { M3u8DLOptions, TsItemInfo, WorkerTaskInfo } from '../types/m3u8';
|
|
3
|
+
export declare const workPoll: WorkerPool<WorkerTaskInfo, {
|
|
4
|
+
success: boolean;
|
|
5
|
+
info: TsItemInfo;
|
|
6
|
+
}>;
|
|
7
|
+
export declare function preDownLoad(url: string, options: M3u8DLOptions): Promise<void>;
|
|
2
8
|
export declare function m3u8Download(url: string, options?: M3u8DLOptions): Promise<{
|
|
3
9
|
options: M3u8DLOptions;
|
|
4
10
|
m3u8Info: {
|
|
@@ -6,7 +12,7 @@ export declare function m3u8Download(url: string, options?: M3u8DLOptions): Prom
|
|
|
6
12
|
tsCount: number;
|
|
7
13
|
durationSecond: number;
|
|
8
14
|
data: TsItemInfo[];
|
|
9
|
-
crypto: import("../
|
|
15
|
+
crypto: import("../types/m3u8").M3u8Crypto;
|
|
10
16
|
};
|
|
11
17
|
filepath: string;
|
|
12
18
|
}>;
|
package/cjs/lib/m3u8-download.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.m3u8Download = void 0;
|
|
3
|
+
exports.m3u8Download = exports.preDownLoad = exports.workPoll = void 0;
|
|
4
4
|
const node_path_1 = require("node:path");
|
|
5
5
|
const node_fs_1 = require("node:fs");
|
|
6
6
|
const node_os_1 = require("node:os");
|
|
@@ -12,15 +12,21 @@ const worker_pool_1 = require("./worker_pool");
|
|
|
12
12
|
const parseM3u8_1 = require("./parseM3u8");
|
|
13
13
|
const m3u8_convert_1 = require("./m3u8-convert");
|
|
14
14
|
const local_play_1 = require("./local-play");
|
|
15
|
+
const cache = {
|
|
16
|
+
m3u8Info: {},
|
|
17
|
+
downloading: new Set(),
|
|
18
|
+
};
|
|
15
19
|
const tsDlFile = (0, node_path_1.resolve)(__dirname, './ts-download.js');
|
|
20
|
+
exports.workPoll = new worker_pool_1.WorkerPool(tsDlFile);
|
|
16
21
|
async function formatOptions(url, opts) {
|
|
17
22
|
const options = {
|
|
18
23
|
delCache: !opts.debug,
|
|
19
24
|
saveDir: process.cwd(),
|
|
25
|
+
showProgress: true,
|
|
20
26
|
...opts,
|
|
21
27
|
};
|
|
22
28
|
if (!url.startsWith('http')) {
|
|
23
|
-
url = url.replace(
|
|
29
|
+
url = url.replace(/\$+/, '|').replace(/\|\|+/, '|');
|
|
24
30
|
if (url.includes('|')) {
|
|
25
31
|
const r = url.split('|');
|
|
26
32
|
url = r[1];
|
|
@@ -39,35 +45,65 @@ async function formatOptions(url, opts) {
|
|
|
39
45
|
options.filename += '.mp4';
|
|
40
46
|
if (!options.cacheDir)
|
|
41
47
|
options.cacheDir = `cache/${urlMd5}`;
|
|
42
|
-
if (!(0, node_fs_1.existsSync)(options.cacheDir))
|
|
43
|
-
await node_fs_1.promises.mkdir(options.cacheDir, { recursive: true });
|
|
44
48
|
if (options.headers)
|
|
45
49
|
utils_1.request.setHeaders(options.headers);
|
|
46
50
|
if (options.debug) {
|
|
47
51
|
utils_1.logger.updateOptions({ levelType: 'debug' });
|
|
48
|
-
utils_1.logger.debug('[m3u8-DL]options', options);
|
|
52
|
+
utils_1.logger.debug('[m3u8-DL]options', options, url);
|
|
49
53
|
}
|
|
50
54
|
return [url, options];
|
|
51
55
|
}
|
|
52
|
-
async function
|
|
53
|
-
utils_1.logger.info('Starting download for', (0, console_log_colors_1.cyanBright)(url));
|
|
56
|
+
async function m3u8InfoParse(url, options = {}) {
|
|
54
57
|
[url, options] = await formatOptions(url, options);
|
|
55
58
|
const ext = (0, utils_1.isSupportFfmpeg)() ? '.mp4' : '.ts';
|
|
56
59
|
let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename);
|
|
57
60
|
if (!filepath.endsWith(ext))
|
|
58
61
|
filepath += ext;
|
|
59
62
|
const result = { options, m3u8Info: null, filepath };
|
|
60
|
-
if (
|
|
61
|
-
|
|
63
|
+
if (cache.m3u8Info[url]) {
|
|
64
|
+
Object.assign(result, cache.m3u8Info[url]);
|
|
62
65
|
return result;
|
|
63
66
|
}
|
|
67
|
+
if (!options.force && (0, node_fs_1.existsSync)(filepath))
|
|
68
|
+
return result;
|
|
64
69
|
const m3u8Info = await (0, parseM3u8_1.parseM3U8)('', url, options.cacheDir).catch(e => utils_1.logger.error(e));
|
|
65
|
-
if (m3u8Info && m3u8Info?.tsCount > 0)
|
|
70
|
+
if (m3u8Info && m3u8Info?.tsCount > 0)
|
|
66
71
|
result.m3u8Info = m3u8Info;
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
async function preDownLoad(url, options) {
|
|
75
|
+
const result = await m3u8InfoParse(url, options);
|
|
76
|
+
if (!result.m3u8Info)
|
|
77
|
+
return;
|
|
78
|
+
for (const info of result.m3u8Info.data) {
|
|
79
|
+
if (!exports.workPoll.freeNum)
|
|
80
|
+
return;
|
|
81
|
+
if (!cache.downloading.has(info.uri)) {
|
|
82
|
+
cache.downloading.add(info.uri);
|
|
83
|
+
exports.workPoll.runTask({ info, options: JSON.parse(JSON.stringify(result.options)), crypto: result.m3u8Info.crypto }, () => {
|
|
84
|
+
cache.downloading.delete(info.uri);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
exports.preDownLoad = preDownLoad;
|
|
90
|
+
async function m3u8Download(url, options = {}) {
|
|
91
|
+
utils_1.logger.info('Starting download for', (0, console_log_colors_1.cyanBright)(url));
|
|
92
|
+
const result = await m3u8InfoParse(url, options);
|
|
93
|
+
options = result.options;
|
|
94
|
+
if (!options.force && (0, node_fs_1.existsSync)(result.filepath) && !result.m3u8Info) {
|
|
95
|
+
utils_1.logger.info('file already exist:', result.filepath);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
if (result.m3u8Info?.tsCount > 0) {
|
|
99
|
+
let n = options.threadNum - exports.workPoll.numThreads;
|
|
100
|
+
if (n > 0)
|
|
101
|
+
while (n--)
|
|
102
|
+
exports.workPoll.addNewWorker();
|
|
103
|
+
const { m3u8Info } = result;
|
|
67
104
|
const startTime = Date.now();
|
|
68
|
-
const pool = new worker_pool_1.WorkerPool(tsDlFile, options.threadNum);
|
|
69
105
|
const barrier = new fe_utils_1.Barrier();
|
|
70
|
-
const playStart = Math.min(options.threadNum + 2, m3u8Info.tsCount);
|
|
106
|
+
const playStart = Math.min(options.threadNum + 2, result.m3u8Info.tsCount);
|
|
71
107
|
const stats = {
|
|
72
108
|
/** 下载成功的 ts 数量 */
|
|
73
109
|
tsSuccess: 0,
|
|
@@ -78,7 +114,7 @@ async function m3u8Download(url, options = {}) {
|
|
|
78
114
|
};
|
|
79
115
|
const runTask = (data) => {
|
|
80
116
|
for (const info of data) {
|
|
81
|
-
|
|
117
|
+
exports.workPoll.runTask({ info, options: JSON.parse(JSON.stringify(options)), crypto: m3u8Info.crypto }, (err, res) => {
|
|
82
118
|
if (!res || err) {
|
|
83
119
|
if (err) {
|
|
84
120
|
console.log('\n');
|
|
@@ -98,26 +134,29 @@ async function m3u8Download(url, options = {}) {
|
|
|
98
134
|
info.tsSize = res.info.tsSize;
|
|
99
135
|
info.success = 1;
|
|
100
136
|
stats.tsSuccess++;
|
|
101
|
-
stats.duration
|
|
137
|
+
stats.duration += info.duration;
|
|
102
138
|
}
|
|
103
139
|
else {
|
|
104
140
|
stats.tsFailed++;
|
|
105
141
|
}
|
|
106
142
|
const finished = stats.tsFailed + stats.tsSuccess;
|
|
107
|
-
if (options.showProgress
|
|
143
|
+
if (options.showProgress) {
|
|
144
|
+
const timeCost = Date.now() - startTime;
|
|
108
145
|
const downloadedSize = m3u8Info.data.reduce((a, b) => a + (b.tsSize || 0), 0);
|
|
109
|
-
const
|
|
146
|
+
const downloadedDuration = m3u8Info.data.reduce((a, b) => a + (b.tsSize ? b.duration : 0), 0);
|
|
147
|
+
const avgSpeed = (0, helper_1.formatByteSize)((downloadedSize / timeCost) * 1000);
|
|
148
|
+
const restTime = downloadedDuration ? (timeCost * (m3u8Info.durationSecond - stats.duration)) / downloadedDuration : 0;
|
|
110
149
|
const percent = Math.floor((finished / m3u8Info.tsCount) * 100);
|
|
111
150
|
const processBar = '='.repeat(Math.floor(percent * 0.2)).padEnd(20, '-');
|
|
112
151
|
utils_1.logger.logInline(`${percent}% [${(0, console_log_colors_1.greenBright)(processBar)}] ${(0, console_log_colors_1.cyan)(finished)} ` +
|
|
113
|
-
`${(0, console_log_colors_1.green)(stats.duration + 'sec')} ` +
|
|
114
|
-
`${(0, console_log_colors_1.blueBright)((0, helper_1.formatByteSize)(downloadedSize))} ${(0, console_log_colors_1.yellowBright)((0, fe_utils_1.formatTimeCost)(startTime))} ${(0, console_log_colors_1.magentaBright)(avgSpeed + '/s')}` +
|
|
115
|
-
(finished === m3u8Info.tsCount ? '\n' : ''));
|
|
152
|
+
`${(0, console_log_colors_1.green)(stats.duration.toFixed(2) + 'sec')} ` +
|
|
153
|
+
`${(0, console_log_colors_1.blueBright)((0, helper_1.formatByteSize)(downloadedSize))} ${(0, console_log_colors_1.yellowBright)((0, fe_utils_1.formatTimeCost)(startTime))} ${(0, console_log_colors_1.magentaBright)(avgSpeed + '/s')} ` +
|
|
154
|
+
(finished === m3u8Info.tsCount ? '\n' : restTime ? `${(0, console_log_colors_1.cyan)((0, fe_utils_1.formatTimeCost)(Date.now() - Math.ceil(restTime)))}` : ''));
|
|
116
155
|
}
|
|
117
156
|
if (options.onProgress)
|
|
118
157
|
options.onProgress(finished, m3u8Info.tsCount, info);
|
|
119
158
|
if (finished === m3u8Info.tsCount) {
|
|
120
|
-
pool.close();
|
|
159
|
+
// pool.close();
|
|
121
160
|
barrier.open();
|
|
122
161
|
}
|
|
123
162
|
if (options.play && finished === playStart) {
|
|
@@ -127,13 +166,13 @@ async function m3u8Download(url, options = {}) {
|
|
|
127
166
|
}
|
|
128
167
|
};
|
|
129
168
|
if (options.showProgress) {
|
|
130
|
-
console.info(
|
|
169
|
+
console.info(`\nTotal segments: ${(0, console_log_colors_1.cyan)(m3u8Info.tsCount)}, duration: ${(0, console_log_colors_1.green)(m3u8Info.durationSecond + 'sec')}.`, `Parallel jobs: ${(0, console_log_colors_1.magenta)(options.threadNum)}`);
|
|
131
170
|
}
|
|
132
171
|
runTask(m3u8Info.data);
|
|
133
172
|
await barrier.wait();
|
|
134
173
|
if (stats.tsFailed === 0) {
|
|
135
174
|
result.filepath = await (0, m3u8_convert_1.m3u8Convert)(options, m3u8Info.data);
|
|
136
|
-
if ((0, node_fs_1.existsSync)(options.cacheDir) && options.delCache)
|
|
175
|
+
if (result.filepath && (0, node_fs_1.existsSync)(options.cacheDir) && options.delCache)
|
|
137
176
|
(0, fe_utils_1.rmrfAsync)(options.cacheDir);
|
|
138
177
|
}
|
|
139
178
|
else
|
package/cjs/lib/parseM3u8.d.ts
CHANGED
package/cjs/lib/parseM3u8.js
CHANGED
|
@@ -19,8 +19,9 @@ async function parseM3U8(content, url = process.cwd(), cacheDir = './cache') {
|
|
|
19
19
|
let parser = new m3u8_parser_1.Parser();
|
|
20
20
|
parser.push(content);
|
|
21
21
|
parser.end();
|
|
22
|
+
utils_1.logger.debug('parser.manifest', parser.manifest);
|
|
22
23
|
if (parser.manifest.playlists?.length > 0) {
|
|
23
|
-
url = new URL(
|
|
24
|
+
url = new URL(parser.manifest.playlists[0].uri, url).toString();
|
|
24
25
|
content = (await (0, utils_1.getRetry)(url)).data;
|
|
25
26
|
parser = new m3u8_parser_1.Parser();
|
|
26
27
|
parser.push(content);
|
|
@@ -70,6 +71,7 @@ async function parseM3U8(content, url = process.cwd(), cacheDir = './cache') {
|
|
|
70
71
|
});
|
|
71
72
|
result.durationSecond += tsList[i].duration;
|
|
72
73
|
}
|
|
74
|
+
result.durationSecond = +Number(result.durationSecond).toFixed(2);
|
|
73
75
|
return result;
|
|
74
76
|
}
|
|
75
77
|
exports.parseM3U8 = parseM3U8;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stor = void 0;
|
|
4
|
+
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const node_os_1 = require("node:os");
|
|
7
|
+
exports.stor = fe_utils_1.LiteStorage.getInstance({ uuid: 'm3u8dl', filepath: (0, node_path_1.resolve)((0, node_os_1.homedir)(), '.liteStorage/m3u8dl.json') });
|
package/cjs/lib/ts-download.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { M3u8Crypto, TsItemInfo } from '../
|
|
1
|
+
import type { M3u8Crypto, TsItemInfo } from '../types/m3u8';
|
|
2
2
|
export declare function tsDownload(info: TsItemInfo, cryptoInfo: M3u8Crypto): Promise<boolean>;
|
package/cjs/lib/ts-download.js
CHANGED
|
@@ -3,7 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.tsDownload = void 0;
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
6
7
|
const node_worker_threads_1 = require("node:worker_threads");
|
|
8
|
+
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
7
9
|
const utils_1 = require("./utils");
|
|
8
10
|
async function tsDownload(info, cryptoInfo) {
|
|
9
11
|
try {
|
|
@@ -13,6 +15,7 @@ async function tsDownload(info, cryptoInfo) {
|
|
|
13
15
|
if (r.response.statusCode === 200) {
|
|
14
16
|
utils_1.logger.debug('\n', info);
|
|
15
17
|
const data = cryptoInfo.key ? aesDecrypt(r.buffer, cryptoInfo) : r.buffer;
|
|
18
|
+
(0, fe_utils_1.mkdirp)((0, node_path_1.dirname)(info.tsOut));
|
|
16
19
|
await node_fs_1.promises.writeFile(info.tsOut, data);
|
|
17
20
|
info.tsSize = r.buffer.byteLength;
|
|
18
21
|
return true;
|
package/cjs/lib/utils.js
CHANGED
|
@@ -5,7 +5,8 @@ const fe_utils_1 = require("@lzwme/fe-utils");
|
|
|
5
5
|
exports.request = new fe_utils_1.Request('', {
|
|
6
6
|
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
7
7
|
});
|
|
8
|
-
|
|
8
|
+
// process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
9
|
+
const getRetry = (url, retries = 3) => (0, fe_utils_1.retry)(() => exports.request.get(url, null, {}, { rejectUnauthorized: false }), 1000, retries, r => r.response.statusCode === 200);
|
|
9
10
|
exports.getRetry = getRetry;
|
|
10
11
|
exports.logger = fe_utils_1.NLogger.getLogger('[M3U8-DL]', { color: fe_utils_1.color });
|
|
11
12
|
let _isSupportFfmpeg = null;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { VideoListResult, VideoSearchResult } from '../types';
|
|
2
|
+
export interface VSOptions {
|
|
3
|
+
/** 播放地址缓存 */
|
|
4
|
+
api?: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare class VideoSearch {
|
|
7
|
+
protected options: VSOptions;
|
|
8
|
+
get api(): string[];
|
|
9
|
+
constructor(options?: VSOptions);
|
|
10
|
+
updateOptions(options: VSOptions): Promise<this>;
|
|
11
|
+
search(wd: string, api?: string): Promise<VideoSearchResult>;
|
|
12
|
+
getVideoList(ids: number | string | (number | string)[], api?: string): Promise<VideoListResult>;
|
|
13
|
+
formatUrl(url: string | string[]): Promise<string[]>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VideoSearch = void 0;
|
|
4
|
+
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
5
|
+
const storage_1 = require("./storage");
|
|
6
|
+
const req = new fe_utils_1.Request(null, {
|
|
7
|
+
'content-type': 'application/json; charset=UTF-8',
|
|
8
|
+
});
|
|
9
|
+
class VideoSearch {
|
|
10
|
+
options;
|
|
11
|
+
get api() {
|
|
12
|
+
return this.options.api;
|
|
13
|
+
}
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.options = options;
|
|
16
|
+
if (!options.api?.length)
|
|
17
|
+
options.api = [];
|
|
18
|
+
if (process.env.VAPI)
|
|
19
|
+
options.api.push(...process.env.VAPI.split('$$$'));
|
|
20
|
+
this.updateOptions(options).then(() => {
|
|
21
|
+
if (!this.api.length)
|
|
22
|
+
throw Error('没有可用站点,请添加或指定');
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async updateOptions(options) {
|
|
26
|
+
const cache = storage_1.stor.get();
|
|
27
|
+
if (Array.isArray(cache.api))
|
|
28
|
+
this.options.api.push(...cache.api);
|
|
29
|
+
if (options.api?.length) {
|
|
30
|
+
this.options.api.unshift(...options.api);
|
|
31
|
+
this.options.api = await this.formatUrl(this.options.api);
|
|
32
|
+
storage_1.stor.set({ api: this.options.api });
|
|
33
|
+
}
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
async search(wd, api = this.api[0]) {
|
|
37
|
+
let { data } = await req.get(api, { wd }, null, { rejectUnauthorized: false });
|
|
38
|
+
if (typeof data == 'string')
|
|
39
|
+
data = JSON.parse(data);
|
|
40
|
+
return data;
|
|
41
|
+
}
|
|
42
|
+
async getVideoList(ids, api = this.api[0]) {
|
|
43
|
+
let { data } = await req.get(api, {
|
|
44
|
+
ac: 'videolist',
|
|
45
|
+
ids: Array.isArray(ids) ? ids.join(',') : ids,
|
|
46
|
+
}, null, { rejectUnauthorized: false });
|
|
47
|
+
if (typeof data == 'string')
|
|
48
|
+
data = JSON.parse(data);
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
async formatUrl(url) {
|
|
52
|
+
const urls = [];
|
|
53
|
+
if (!url)
|
|
54
|
+
return urls;
|
|
55
|
+
if (typeof url === 'string')
|
|
56
|
+
url = [url];
|
|
57
|
+
for (let u of url) {
|
|
58
|
+
u = String(u || '').trim();
|
|
59
|
+
if (!u)
|
|
60
|
+
continue;
|
|
61
|
+
if (u.endsWith('.json')) {
|
|
62
|
+
const { data } = await req.get(u, null, null, { rejectUnauthorized: false });
|
|
63
|
+
if (Array.isArray(data)) {
|
|
64
|
+
urls.push(...(await this.formatUrl(data)));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
urls.push(...Object.values(data));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (u.startsWith('http')) {
|
|
71
|
+
if (u.endsWith('provide/'))
|
|
72
|
+
u += 'vod/';
|
|
73
|
+
if (u.endsWith('provide/vod'))
|
|
74
|
+
u += '/';
|
|
75
|
+
urls.push(u.replace('/at/xml/', '/'));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return [...new Set(urls)];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.VideoSearch = VideoSearch;
|
|
82
|
+
// const v = new VideoSearch({ api: ['https://api.xinlangapi.com/xinlangapi.php/provide/vod/'] });
|
|
83
|
+
// v.search('三体')
|
|
84
|
+
// .then(d => {
|
|
85
|
+
// console.log(d.total, d.list);
|
|
86
|
+
// return v.getVideoList(d.list[0].vod_id);
|
|
87
|
+
// })
|
|
88
|
+
// .then(d => {
|
|
89
|
+
// console.log('detail:', d.total, d.list[0]);
|
|
90
|
+
// });
|
package/cjs/lib/worker_pool.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export declare class WorkerPool<T = unknown, R = unknown> extends EventEmitter {
|
|
|
7
7
|
private freeWorkers;
|
|
8
8
|
private workerTaskInfo;
|
|
9
9
|
private tasks;
|
|
10
|
+
get totalTask(): number;
|
|
11
|
+
get totalNum(): number;
|
|
12
|
+
get freeNum(): number;
|
|
10
13
|
constructor(processorFile: string, numThreads?: number);
|
|
11
14
|
addNewWorker(processorFile?: string): void;
|
|
12
15
|
runTask(task: T, callback: (err: Error | null, result: R) => void): void;
|
package/cjs/lib/worker_pool.js
CHANGED
|
@@ -26,6 +26,15 @@ class WorkerPool extends node_events_1.EventEmitter {
|
|
|
26
26
|
freeWorkers = [];
|
|
27
27
|
workerTaskInfo = new Map();
|
|
28
28
|
tasks = [];
|
|
29
|
+
get totalTask() {
|
|
30
|
+
return this.tasks.length;
|
|
31
|
+
}
|
|
32
|
+
get totalNum() {
|
|
33
|
+
return this.workers.length;
|
|
34
|
+
}
|
|
35
|
+
get freeNum() {
|
|
36
|
+
return this.freeWorkers.length;
|
|
37
|
+
}
|
|
29
38
|
constructor(processorFile, numThreads = 0) {
|
|
30
39
|
super();
|
|
31
40
|
this.processorFile = processorFile;
|
|
@@ -51,10 +60,10 @@ class WorkerPool extends node_events_1.EventEmitter {
|
|
|
51
60
|
// 如果成功:调用传递给`runTask`的回调,删除与Worker关联的`TaskInfo`,并再次将其标记为空闲。
|
|
52
61
|
const r = this.workerTaskInfo.get(worker);
|
|
53
62
|
if (r) {
|
|
54
|
-
r.done(null, result);
|
|
55
63
|
this.workerTaskInfo.delete(worker);
|
|
56
64
|
this.freeWorkers.push(worker);
|
|
57
65
|
this.emit(kWorkerFreedEvent);
|
|
66
|
+
r.done(null, result);
|
|
58
67
|
}
|
|
59
68
|
});
|
|
60
69
|
worker.on('error', err => {
|
|
@@ -73,6 +82,8 @@ class WorkerPool extends node_events_1.EventEmitter {
|
|
|
73
82
|
this.workers.push(worker);
|
|
74
83
|
this.freeWorkers.push(worker);
|
|
75
84
|
this.emit(kWorkerFreedEvent);
|
|
85
|
+
if (this.numThreads < this.workers.length)
|
|
86
|
+
this.numThreads = this.workers.length;
|
|
76
87
|
}
|
|
77
88
|
runTask(task, callback) {
|
|
78
89
|
if (this.freeWorkers.length === 0) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { M3u8DLOptions } from './
|
|
1
|
+
import { M3u8DLOptions } from './types/m3u8';
|
|
2
2
|
export declare function m3u8BatchDownload(urls: string[], options: M3u8DLOptions): Promise<void>;
|
|
@@ -4,7 +4,9 @@ exports.m3u8BatchDownload = void 0;
|
|
|
4
4
|
const node_fs_1 = require("node:fs");
|
|
5
5
|
const node_path_1 = require("node:path");
|
|
6
6
|
const m3u8_download_1 = require("./lib/m3u8-download");
|
|
7
|
-
|
|
7
|
+
const utils_1 = require("./lib/utils");
|
|
8
|
+
async function formatUrls(urls, options) {
|
|
9
|
+
const taskset = new Map();
|
|
8
10
|
for (const url of urls) {
|
|
9
11
|
if ((0, node_fs_1.existsSync)(url)) {
|
|
10
12
|
const content = await node_fs_1.promises.readFile(url, 'utf8');
|
|
@@ -17,13 +19,42 @@ async function m3u8BatchDownload(urls, options) {
|
|
|
17
19
|
href = `${idx}|${href}`;
|
|
18
20
|
return href;
|
|
19
21
|
});
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
const o = { ...options };
|
|
23
|
+
if (!o.filename)
|
|
24
|
+
o.filename = (0, node_path_1.basename)(url).split('.')[0];
|
|
25
|
+
const t = await formatUrls(list, o);
|
|
26
|
+
for (const d of t.entries())
|
|
27
|
+
taskset.set(d[0], d[1]);
|
|
23
28
|
continue;
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
|
-
|
|
31
|
+
taskset.set(url, options);
|
|
27
32
|
}
|
|
33
|
+
return taskset;
|
|
34
|
+
}
|
|
35
|
+
async function m3u8BatchDownload(urls, options) {
|
|
36
|
+
const tasks = await formatUrls(urls, options);
|
|
37
|
+
return new Promise(rs => {
|
|
38
|
+
let preDLing = false;
|
|
39
|
+
const run = async () => {
|
|
40
|
+
const [key, keyNext] = [...tasks.keys()];
|
|
41
|
+
if (key) {
|
|
42
|
+
const o = { ...tasks.get(key) };
|
|
43
|
+
tasks.delete(key);
|
|
44
|
+
const p = o.onProgress;
|
|
45
|
+
o.onProgress = (finished, total, info) => {
|
|
46
|
+
if (p)
|
|
47
|
+
p(finished, total, info);
|
|
48
|
+
if (!preDLing && keyNext && tasks.size && m3u8_download_1.workPoll.freeNum > 1 && total - finished < options.threadNum) {
|
|
49
|
+
utils_1.logger.debug('\n[预下载下一集]', 'freeNum:', m3u8_download_1.workPoll.freeNum, 'totalNum:', m3u8_download_1.workPoll.totalNum, 'totalTask:', m3u8_download_1.workPoll.totalTask, tasks.size);
|
|
50
|
+
preDLing = true;
|
|
51
|
+
(0, m3u8_download_1.preDownLoad)(keyNext, options).then(() => (preDLing = false));
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
(0, m3u8_download_1.m3u8Download)(key, o).then(() => (tasks.size === 0 ? rs() : run()));
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
run();
|
|
58
|
+
});
|
|
28
59
|
}
|
|
29
60
|
exports.m3u8BatchDownload = m3u8BatchDownload;
|
|
@@ -0,0 +1,18 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./m3u8"), exports);
|
|
18
|
+
__exportStar(require("./video-search"), exports);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** 模糊搜索返回的结果(?wd=<wd>) */
|
|
2
|
+
export interface VideoSearchResult {
|
|
3
|
+
code: number;
|
|
4
|
+
msg: string;
|
|
5
|
+
page: number;
|
|
6
|
+
pagecount: number;
|
|
7
|
+
limit: string;
|
|
8
|
+
total: number;
|
|
9
|
+
list: VodList[];
|
|
10
|
+
class: {
|
|
11
|
+
type_id: number;
|
|
12
|
+
type_pid: number;
|
|
13
|
+
type_name: string;
|
|
14
|
+
}[];
|
|
15
|
+
}
|
|
16
|
+
interface VodList {
|
|
17
|
+
vod_id: number;
|
|
18
|
+
vod_name: string;
|
|
19
|
+
type_id: number;
|
|
20
|
+
type_name: string;
|
|
21
|
+
vod_en: string;
|
|
22
|
+
vod_time: string;
|
|
23
|
+
vod_remarks: string;
|
|
24
|
+
vod_play_from: string;
|
|
25
|
+
vod_play_url: string;
|
|
26
|
+
}
|
|
27
|
+
/** 按 id 搜素返回的详情列表 */
|
|
28
|
+
export interface VideoListResult {
|
|
29
|
+
code: number;
|
|
30
|
+
msg: string;
|
|
31
|
+
page: number;
|
|
32
|
+
pagecount: number;
|
|
33
|
+
limit: string;
|
|
34
|
+
total: number;
|
|
35
|
+
list: VideoDetails[];
|
|
36
|
+
}
|
|
37
|
+
export interface VideoDetails {
|
|
38
|
+
vod_id: number;
|
|
39
|
+
type_id: number;
|
|
40
|
+
type_id_1: number;
|
|
41
|
+
group_id: number;
|
|
42
|
+
vod_name: string;
|
|
43
|
+
vod_sub: string;
|
|
44
|
+
vod_en: string;
|
|
45
|
+
vod_status: number;
|
|
46
|
+
vod_letter: string;
|
|
47
|
+
vod_color: string;
|
|
48
|
+
vod_tag: string;
|
|
49
|
+
vod_class: string;
|
|
50
|
+
vod_pic: string;
|
|
51
|
+
vod_pic_thumb: string;
|
|
52
|
+
vod_pic_slide: string;
|
|
53
|
+
vod_pic_screenshot: null;
|
|
54
|
+
vod_actor: string;
|
|
55
|
+
vod_director: string;
|
|
56
|
+
vod_writer: string;
|
|
57
|
+
vod_behind: string;
|
|
58
|
+
vod_blurb: string;
|
|
59
|
+
vod_remarks: string;
|
|
60
|
+
vod_pubdate: string;
|
|
61
|
+
vod_total: number;
|
|
62
|
+
vod_serial: string;
|
|
63
|
+
vod_tv: string;
|
|
64
|
+
vod_weekday: string;
|
|
65
|
+
vod_area: string;
|
|
66
|
+
vod_lang: string;
|
|
67
|
+
vod_year: string;
|
|
68
|
+
vod_version: string;
|
|
69
|
+
vod_state: string;
|
|
70
|
+
vod_author: string;
|
|
71
|
+
vod_jumpurl: string;
|
|
72
|
+
vod_tpl: string;
|
|
73
|
+
vod_tpl_play: string;
|
|
74
|
+
vod_tpl_down: string;
|
|
75
|
+
vod_isend: number;
|
|
76
|
+
vod_lock: number;
|
|
77
|
+
vod_level: number;
|
|
78
|
+
vod_copyright: number;
|
|
79
|
+
vod_points: number;
|
|
80
|
+
vod_points_play: number;
|
|
81
|
+
vod_points_down: number;
|
|
82
|
+
vod_hits: number;
|
|
83
|
+
vod_hits_day: number;
|
|
84
|
+
vod_hits_week: number;
|
|
85
|
+
vod_hits_month: number;
|
|
86
|
+
vod_duration: string;
|
|
87
|
+
vod_up: number;
|
|
88
|
+
vod_down: number;
|
|
89
|
+
vod_score: string;
|
|
90
|
+
vod_score_all: number;
|
|
91
|
+
vod_score_num: number;
|
|
92
|
+
vod_time: string;
|
|
93
|
+
vod_time_add: number;
|
|
94
|
+
vod_time_hits: number;
|
|
95
|
+
vod_time_make: number;
|
|
96
|
+
vod_trysee: number;
|
|
97
|
+
vod_douban_id: number;
|
|
98
|
+
vod_douban_score: string;
|
|
99
|
+
vod_reurl: string;
|
|
100
|
+
vod_rel_vod: string;
|
|
101
|
+
vod_rel_art: string;
|
|
102
|
+
vod_pwd: string;
|
|
103
|
+
vod_pwd_url: string;
|
|
104
|
+
vod_pwd_play: string;
|
|
105
|
+
vod_pwd_play_url: string;
|
|
106
|
+
vod_pwd_down: string;
|
|
107
|
+
vod_pwd_down_url: string;
|
|
108
|
+
vod_content: string;
|
|
109
|
+
vod_play_from: string;
|
|
110
|
+
vod_play_server: string;
|
|
111
|
+
vod_play_note: string;
|
|
112
|
+
vod_play_url: string;
|
|
113
|
+
vod_down_from: string;
|
|
114
|
+
vod_down_server: string;
|
|
115
|
+
vod_down_note: string;
|
|
116
|
+
vod_down_url: string;
|
|
117
|
+
vod_plot: number;
|
|
118
|
+
vod_plot_name: string;
|
|
119
|
+
vod_plot_detail: string;
|
|
120
|
+
type_name: string;
|
|
121
|
+
}
|
|
122
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lzwme/m3u8-dl",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Batch download of m3u8 files and convert to mp4",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"types": "cjs/index.d.ts",
|
|
@@ -44,23 +44,24 @@
|
|
|
44
44
|
"registry": "https://registry.npmjs.com"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@lzwme/fed-lint-helper": "^2.2
|
|
48
|
-
"@types/node": "^
|
|
49
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
50
|
-
"@typescript-eslint/parser": "^5.
|
|
51
|
-
"eslint": "^8.
|
|
52
|
-
"eslint-config-prettier": "^8.
|
|
47
|
+
"@lzwme/fed-lint-helper": "^2.3.2",
|
|
48
|
+
"@types/node": "^20.2.0",
|
|
49
|
+
"@typescript-eslint/eslint-plugin": "^5.59.6",
|
|
50
|
+
"@typescript-eslint/parser": "^5.59.6",
|
|
51
|
+
"eslint": "^8.40.0",
|
|
52
|
+
"eslint-config-prettier": "^8.8.0",
|
|
53
53
|
"eslint-plugin-prettier": "^4.2.1",
|
|
54
54
|
"husky": "^8.0.3",
|
|
55
|
-
"prettier": "^2.8.
|
|
55
|
+
"prettier": "^2.8.8",
|
|
56
56
|
"standard-version": "^9.5.0",
|
|
57
|
-
"typescript": "^
|
|
57
|
+
"typescript": "^5.0.4"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@lzwme/fe-utils": "^1.
|
|
61
|
-
"commander": "^10.0.
|
|
62
|
-
"console-log-colors": "^0.
|
|
63
|
-
"
|
|
60
|
+
"@lzwme/fe-utils": "^1.5.1",
|
|
61
|
+
"commander": "^10.0.1",
|
|
62
|
+
"console-log-colors": "^0.4.0",
|
|
63
|
+
"enquirer": "^2.3.6",
|
|
64
|
+
"m3u8-parser": "^6.1.0"
|
|
64
65
|
},
|
|
65
66
|
"files": [
|
|
66
67
|
"cjs",
|