@lzwme/m3u8-dl 1.2.0 → 1.2.2
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 +1 -0
- package/cjs/cli.js +16 -15
- package/cjs/lib/file-download.d.ts +1 -1
- package/cjs/lib/file-download.js +11 -9
- package/cjs/lib/format-options.d.ts +1 -1
- package/cjs/lib/format-options.js +3 -3
- package/cjs/lib/local-play.js +11 -11
- package/cjs/lib/m3u8-convert.js +1 -1
- package/cjs/lib/m3u8-download.js +29 -26
- package/cjs/lib/parseM3u8.d.ts +1 -1
- package/cjs/lib/parseM3u8.js +37 -28
- package/cjs/lib/search-api/CommSearchApi.d.ts +1 -1
- package/cjs/lib/search-api/CommSearchApi.js +2 -2
- package/cjs/lib/storage.js +2 -2
- package/cjs/lib/ts-download.d.ts +1 -1
- package/cjs/lib/ts-download.js +3 -2
- package/cjs/lib/utils.d.ts +1 -1
- package/cjs/lib/utils.js +4 -4
- package/cjs/lib/video-search.js +56 -56
- package/cjs/lib/worker_pool.js +3 -3
- package/cjs/m3u8-batch-download.d.ts +1 -1
- package/cjs/m3u8-batch-download.js +11 -13
- package/cjs/server/download-server.d.ts +6 -3
- package/cjs/server/download-server.js +67 -35
- package/cjs/types/index.js +0 -2
- package/cjs/types/m3u8.d.ts +6 -2
- package/cjs/types/video-search.d.ts +1 -1
- package/cjs/video-parser/index.js +5 -5
- package/cjs/video-parser/parsers/base-parser.js +1 -1
- package/cjs/video-parser/parsers/douyin-parser.d.ts +1 -1
- package/cjs/video-parser/parsers/douyin-parser.js +8 -8
- package/cjs/video-parser/parsers/pipixia-parser.d.ts +1 -1
- package/cjs/video-parser/parsers/pipixia-parser.js +7 -7
- package/cjs/video-parser/parsers/weibo-parser.d.ts +1 -1
- package/cjs/video-parser/parsers/weibo-parser.js +12 -12
- package/client/index.html +417 -258
- package/package.json +14 -12
- package/global.d.ts +0 -1
package/README.MD
CHANGED
package/cjs/cli.js
CHANGED
|
@@ -34,12 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
const node_path_1 = require("node:path");
|
|
37
|
+
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
37
38
|
const commander_1 = require("commander");
|
|
38
39
|
const console_log_colors_1 = require("console-log-colors");
|
|
39
|
-
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
40
40
|
const utils_js_1 = require("./lib/utils.js");
|
|
41
|
-
const m3u8_batch_download_1 = require("./m3u8-batch-download");
|
|
42
41
|
const video_search_js_1 = require("./lib/video-search.js");
|
|
42
|
+
const m3u8_batch_download_1 = require("./m3u8-batch-download");
|
|
43
43
|
const pkg = (0, fe_utils_1.readJsonFileSync)((0, node_path_1.resolve)(__dirname, '../package.json'));
|
|
44
44
|
process.on('unhandledRejection', r => {
|
|
45
45
|
console.error(r);
|
|
@@ -54,19 +54,19 @@ commander_1.program
|
|
|
54
54
|
.version(pkg.version, '-v, --version')
|
|
55
55
|
.description((0, console_log_colors_1.cyanBright)(pkg.description))
|
|
56
56
|
.argument('<m3u8Urls...>', 'm3u8 url。也可以是本地 txt 文件,指定一组 m3u8,适用于批量下载的场景')
|
|
57
|
-
.option('--silent',
|
|
58
|
-
.option('--debug',
|
|
59
|
-
.option('-f, --filename <name>',
|
|
60
|
-
.option('-n, --thread-num <number>',
|
|
61
|
-
.option('-F, --force',
|
|
62
|
-
.option('--no-progress',
|
|
63
|
-
.option('-p, --play',
|
|
64
|
-
.option('-C, --cache-dir <dirpath>',
|
|
65
|
-
.option('-S, --save-dir <dirpath>',
|
|
66
|
-
.option('--no-del-cache',
|
|
57
|
+
.option('--silent', '开启静默模式。')
|
|
58
|
+
.option('--debug', '开启调试模式。')
|
|
59
|
+
.option('-f, --filename <name>', '指定下载文件的保存名称。默认取 url md5 值。若指定了多个 url 地址,则会在末尾增加序号')
|
|
60
|
+
.option('-n, --thread-num <number>', '并发下载线程数。默认为 cpu * 2。可设置不同数值观察下载效果')
|
|
61
|
+
.option('-F, --force', '启用强制执行模式。文件已存在时,是否仍继续下载和生成')
|
|
62
|
+
.option('--no-progress', '是否不打印进度信息')
|
|
63
|
+
.option('-p, --play', '是否边下边看')
|
|
64
|
+
.option('-C, --cache-dir <dirpath>', '临时文件保存目录。默认为 cache')
|
|
65
|
+
.option('-S, --save-dir <dirpath>', '下载文件保存的路径。默认为当前目录')
|
|
66
|
+
.option('--no-del-cache', '下载成功后是否删除临时文件。默认为 true。保存临时文件可以在重复下载时识别缓存')
|
|
67
67
|
.option('--no-convert', '下载成功后,是否不合并转换为 mp4 文件。默认为 true。')
|
|
68
|
-
.option('-H, --headers <headers>',
|
|
69
|
-
.option('-T, --type <type>',
|
|
68
|
+
.option('-H, --headers <headers>', '自定义请求头。格式为 key1=value1\nkey2=value2')
|
|
69
|
+
.option('-T, --type <type>', '指定下载类型。默认根据URL自动识别,如果是批量下载多个不同 URL 类型,请不要设置。可选值:m3u8, file, parser')
|
|
70
70
|
.action(async (urls) => {
|
|
71
71
|
const options = getOptions();
|
|
72
72
|
utils_js_1.logger.debug(urls, options);
|
|
@@ -83,11 +83,12 @@ commander_1.program
|
|
|
83
83
|
.command('server')
|
|
84
84
|
.description('启动下载中心web服务')
|
|
85
85
|
.option('-P, --port <port>', '指定web服务端口。默认为6600')
|
|
86
|
-
.option('--token <token>', '指定web服务密码(请求头authorization)。默认为空')
|
|
86
|
+
.option('-t, --token <token>', '指定web服务密码(请求头authorization)。默认为空')
|
|
87
87
|
.action((options) => {
|
|
88
88
|
const opts = getOptions();
|
|
89
89
|
if (opts.debug)
|
|
90
90
|
options.debug = true;
|
|
91
|
+
console.log(opts, options);
|
|
91
92
|
Promise.resolve().then(() => __importStar(require('./server/download-server.js'))).then(m => {
|
|
92
93
|
new m.DLServer(options);
|
|
93
94
|
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { M3u8DLOptions, M3u8DLResult } from '../types';
|
|
2
|
-
export declare function fileDownload(
|
|
2
|
+
export declare function fileDownload(u: string, opts: M3u8DLOptions): Promise<M3u8DLResult>;
|
package/cjs/lib/file-download.js
CHANGED
|
@@ -4,11 +4,11 @@ exports.fileDownload = fileDownload;
|
|
|
4
4
|
const node_path_1 = require("node:path");
|
|
5
5
|
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
6
6
|
const console_log_colors_1 = require("console-log-colors");
|
|
7
|
-
const utils_js_1 = require("./utils.js");
|
|
8
7
|
const format_options_js_1 = require("./format-options.js");
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
const utils_js_1 = require("./utils.js");
|
|
9
|
+
async function fileDownload(u, opts) {
|
|
10
|
+
utils_js_1.logger.debug('fileDownload', u, opts);
|
|
11
|
+
const [url, options] = (0, format_options_js_1.formatOptions)(u, opts);
|
|
12
12
|
const startTime = Date.now();
|
|
13
13
|
const stats = {
|
|
14
14
|
url,
|
|
@@ -49,13 +49,15 @@ async function fileDownload(url, options) {
|
|
|
49
49
|
stats.size = info.size;
|
|
50
50
|
stats.downloadedSize = info.downloaded;
|
|
51
51
|
stats.speed = info.speed;
|
|
52
|
-
stats.speedDesc = (0, fe_utils_1.formatByteSize)(stats.speed)
|
|
52
|
+
stats.speedDesc = `${(0, fe_utils_1.formatByteSize)(stats.speed)}/s`;
|
|
53
53
|
stats.remainingTime = Math.round((info.size - info.downloaded) / stats.speed);
|
|
54
54
|
if (options.showProgress) {
|
|
55
55
|
const processBar = info.percent === -1 ? '' : '='.repeat(Math.floor(stats.progress * 0.2)).padEnd(20, '-');
|
|
56
|
-
utils_js_1.logger.logInline(
|
|
57
|
-
`${
|
|
58
|
-
(
|
|
56
|
+
utils_js_1.logger.logInline([
|
|
57
|
+
`${stats.progress}% [${(0, console_log_colors_1.greenBright)(processBar)}] `,
|
|
58
|
+
`${(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)} `,
|
|
59
|
+
stats.progress === 100 ? '\n' : stats.remainingTime ? `${(0, console_log_colors_1.cyan)((0, fe_utils_1.formatTimeCost)(Date.now() - stats.remainingTime))}` : '',
|
|
60
|
+
].join(''));
|
|
59
61
|
}
|
|
60
62
|
if (options.onProgress) {
|
|
61
63
|
return options.onProgress(info.downloaded, info.size, null, stats);
|
|
@@ -74,7 +76,7 @@ async function fileDownload(url, options) {
|
|
|
74
76
|
stats.errmsg = error.message;
|
|
75
77
|
return {
|
|
76
78
|
isExist: false,
|
|
77
|
-
errmsg:
|
|
79
|
+
errmsg: `下载失败: ${error.message}`,
|
|
78
80
|
stats,
|
|
79
81
|
};
|
|
80
82
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { M3u8DLOptions } from '../types';
|
|
1
|
+
import type { M3u8DLOptions } from '../types';
|
|
2
2
|
export declare function formatOptions(url: string, opts: M3u8DLOptions): readonly [string, M3u8DLOptions, string];
|
|
@@ -4,8 +4,8 @@ exports.formatOptions = formatOptions;
|
|
|
4
4
|
const node_os_1 = require("node:os");
|
|
5
5
|
const node_path_1 = require("node:path");
|
|
6
6
|
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
7
|
-
const utils_1 = require("./utils");
|
|
8
7
|
const video_parser_1 = require("../video-parser");
|
|
8
|
+
const utils_1 = require("./utils");
|
|
9
9
|
const fileSupportExtList = [
|
|
10
10
|
'.mp4',
|
|
11
11
|
'.mkv',
|
|
@@ -48,7 +48,7 @@ function formatOptions(url, opts) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
let [u, n] = url.split(/[|$]+/);
|
|
51
|
-
if (n
|
|
51
|
+
if (n?.startsWith('http'))
|
|
52
52
|
[u, n] = [n, u];
|
|
53
53
|
url = u;
|
|
54
54
|
if (n) {
|
|
@@ -67,7 +67,7 @@ function formatOptions(url, opts) {
|
|
|
67
67
|
if (!(0, node_path_1.extname)(options.filename) && options.type !== 'file')
|
|
68
68
|
options.filename += ext || '.mp4';
|
|
69
69
|
if (!options.cacheDir)
|
|
70
|
-
options.cacheDir =
|
|
70
|
+
options.cacheDir = 'cache';
|
|
71
71
|
if (options.headers)
|
|
72
72
|
options.headers = (0, utils_1.formatHeaders)(options.headers);
|
|
73
73
|
if (!options.threadNum || +options.threadNum <= 0)
|
package/cjs/lib/local-play.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.localPlay = localPlay;
|
|
4
4
|
exports.toLocalM3u8 = toLocalM3u8;
|
|
5
|
-
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
6
|
-
const console_log_colors_1 = require("console-log-colors");
|
|
7
5
|
const node_fs_1 = require("node:fs");
|
|
8
6
|
const node_http_1 = require("node:http");
|
|
9
7
|
const node_path_1 = require("node:path");
|
|
8
|
+
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
9
|
+
const console_log_colors_1 = require("console-log-colors");
|
|
10
10
|
const utils_js_1 = require("./utils.js");
|
|
11
11
|
/**
|
|
12
12
|
* 边下边看
|
|
@@ -32,24 +32,24 @@ function toLocalM3u8(m3u8Info, m3u8FilePath = '', host = '') {
|
|
|
32
32
|
if ((0, node_fs_1.existsSync)(m3u8FilePath))
|
|
33
33
|
return m3u8FilePath;
|
|
34
34
|
const m3u8ContentList = [
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
'#EXTM3U',
|
|
36
|
+
'#EXT-X-VERSION:3',
|
|
37
|
+
'#EXT-X-ALLOW-CACHE:YES',
|
|
38
38
|
`#EXT-X-TARGETDURATION:${Math.max(...m3u8Info.map(d => d.duration))}`,
|
|
39
|
-
|
|
39
|
+
'#EXT-X-MEDIA-SEQUENCE:0',
|
|
40
40
|
// `#EXT-X-KEY:METHOD=AES-128,URI="/api/aes/enc.key"`,
|
|
41
41
|
];
|
|
42
42
|
if (host && !host.endsWith('/'))
|
|
43
43
|
host += '/';
|
|
44
|
-
|
|
44
|
+
for (const d of m3u8Info) {
|
|
45
45
|
if (d.tsOut)
|
|
46
46
|
m3u8ContentList.push(`#EXTINF:${Number(d.duration).toFixed(6)},`, `${host}${(0, node_path_1.basename)(d.tsOut)}`);
|
|
47
|
-
}
|
|
48
|
-
m3u8ContentList.push(
|
|
47
|
+
}
|
|
48
|
+
m3u8ContentList.push('#EXT-X-ENDLIST');
|
|
49
49
|
const m3u8Content = m3u8ContentList.join('\n');
|
|
50
50
|
const ext = (0, node_path_1.extname)(m3u8FilePath);
|
|
51
51
|
if (ext !== '.m3u8')
|
|
52
|
-
m3u8FilePath = m3u8FilePath.replace(ext, '')
|
|
52
|
+
m3u8FilePath = `${m3u8FilePath.replace(ext, '')}.m3u8`;
|
|
53
53
|
m3u8FilePath = (0, node_path_1.resolve)(cacheDir, m3u8FilePath);
|
|
54
54
|
(0, fe_utils_1.mkdirp)(cacheDir);
|
|
55
55
|
(0, node_fs_1.writeFileSync)(m3u8FilePath, m3u8Content, 'utf8');
|
|
@@ -79,7 +79,7 @@ async function createLocalServer(baseDir) {
|
|
|
79
79
|
(0, node_fs_1.createReadStream)(filename).pipe(res);
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
if (stats.isDirectory()) {
|
|
83
83
|
const html = (0, node_fs_1.readdirSync)(filename).map(fname => {
|
|
84
84
|
const rpath = (0, node_path_1.resolve)(filename, fname).replace(baseDir, '');
|
|
85
85
|
return `<li><a href="${rpath}">${rpath}</a></li>`;
|
package/cjs/lib/m3u8-convert.js
CHANGED
|
@@ -20,7 +20,7 @@ async function m3u8Convert(options, data) {
|
|
|
20
20
|
let filesAllArr = data.filter(d => (0, node_fs_1.existsSync)(d.tsOut)).map(d => `file '${d.tsOut}'\nduration ${d.duration}`);
|
|
21
21
|
if (process.platform === 'win32')
|
|
22
22
|
filesAllArr = filesAllArr.map(d => d.replaceAll('\\', '/'));
|
|
23
|
-
(0, node_fs_1.writeFileSync)(ffconcatFile,
|
|
23
|
+
(0, node_fs_1.writeFileSync)(ffconcatFile, `ffconcat version 1.0\n${filesAllArr.join('\n')}`);
|
|
24
24
|
let headersString = '';
|
|
25
25
|
if (options.headers) {
|
|
26
26
|
for (const [key, value] of Object.entries(options.headers)) {
|
package/cjs/lib/m3u8-download.js
CHANGED
|
@@ -4,17 +4,17 @@ exports.workPollPublic = exports.downloadQueue = exports.DownloadQueue = void 0;
|
|
|
4
4
|
exports.preDownLoad = preDownLoad;
|
|
5
5
|
exports.m3u8Download = m3u8Download;
|
|
6
6
|
exports.m3u8DLStop = m3u8DLStop;
|
|
7
|
-
const node_path_1 = require("node:path");
|
|
8
7
|
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
9
|
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
|
-
const utils_js_1 = require("./utils.js");
|
|
13
12
|
const format_options_js_1 = require("./format-options.js");
|
|
14
|
-
const worker_pool_js_1 = require("./worker_pool.js");
|
|
15
|
-
const parseM3u8_js_1 = require("./parseM3u8.js");
|
|
16
|
-
const m3u8_convert_js_1 = require("./m3u8-convert.js");
|
|
17
13
|
const local_play_js_1 = require("./local-play.js");
|
|
14
|
+
const m3u8_convert_js_1 = require("./m3u8-convert.js");
|
|
15
|
+
const parseM3u8_js_1 = require("./parseM3u8.js");
|
|
16
|
+
const utils_js_1 = require("./utils.js");
|
|
17
|
+
const worker_pool_js_1 = require("./worker_pool.js");
|
|
18
18
|
/** 下载队列管理 */
|
|
19
19
|
class DownloadQueue {
|
|
20
20
|
queue = [];
|
|
@@ -101,7 +101,10 @@ async function m3u8InfoParse(url, options = {}) {
|
|
|
101
101
|
}
|
|
102
102
|
if (!options.force && (0, node_fs_1.existsSync)(filepath))
|
|
103
103
|
return result;
|
|
104
|
-
const m3u8Info = await (0, parseM3u8_js_1.parseM3U8)(url, (0, node_path_1.resolve)(options.cacheDir, urlMd5), options.headers).catch(e =>
|
|
104
|
+
const m3u8Info = await (0, parseM3u8_js_1.parseM3U8)(url, (0, node_path_1.resolve)(options.cacheDir, urlMd5), options.headers).catch(e => {
|
|
105
|
+
utils_js_1.logger.error('[parseM3U8][failed]', e.message);
|
|
106
|
+
console.log(e);
|
|
107
|
+
});
|
|
105
108
|
if (m3u8Info && m3u8Info?.tsCount > 0)
|
|
106
109
|
result.m3u8Info = m3u8Info;
|
|
107
110
|
return result;
|
|
@@ -132,7 +135,7 @@ async function preDownLoad(url, options, wp = exports.workPollPublic) {
|
|
|
132
135
|
return;
|
|
133
136
|
if (!cache.downloading.has(info.uri)) {
|
|
134
137
|
cache.downloading.add(info.uri);
|
|
135
|
-
wp.runTask({ url, info, options: JSON.parse(JSON.stringify(result.options)), crypto: result.m3u8Info.crypto }, () => {
|
|
138
|
+
wp.runTask({ url, info, options: JSON.parse(JSON.stringify(result.options)), crypto: result.m3u8Info.crypto[info.keyUri] }, () => {
|
|
136
139
|
cache.downloading.delete(info.uri);
|
|
137
140
|
});
|
|
138
141
|
}
|
|
@@ -196,7 +199,7 @@ async function m3u8Download(url, options = {}) {
|
|
|
196
199
|
const runTask = (data) => {
|
|
197
200
|
for (const info of data) {
|
|
198
201
|
const o = JSON.parse(JSON.stringify(options));
|
|
199
|
-
workPoll.runTask({ url, info, options: o, crypto: m3u8Info.crypto }, (err, res, taskStartTime) => {
|
|
202
|
+
workPoll.runTask({ url, info, options: o, crypto: m3u8Info.crypto[info.keyUri] }, (err, res, taskStartTime) => {
|
|
200
203
|
stats.errmsg = err ? err.cause || err.message || err.toString() : '';
|
|
201
204
|
if (!res || err) {
|
|
202
205
|
if (err) {
|
|
@@ -230,28 +233,26 @@ async function m3u8Download(url, options = {}) {
|
|
|
230
233
|
const downloadedDuration = m3u8Info.data.reduce((a, b) => a + (b.tsSize ? b.duration : 0), 0);
|
|
231
234
|
stats.downloadedSize = m3u8Info.data.reduce((a, b) => a + (b.tsSize || 0), 0);
|
|
232
235
|
stats.avgSpeed = (stats.downloadedSize / timeCost) * 1000;
|
|
233
|
-
stats.avgSpeedDesc = (0, helper_1.formatByteSize)(stats.avgSpeed)
|
|
236
|
+
stats.avgSpeedDesc = `${(0, helper_1.formatByteSize)(stats.avgSpeed)}/s`;
|
|
234
237
|
// 如果当前速度小于平均速度,则更新为平均速度
|
|
235
238
|
if (stats.speed < stats.avgSpeed)
|
|
236
239
|
stats.speed = stats.avgSpeed;
|
|
237
|
-
stats.speedDesc = (0, helper_1.formatByteSize)(stats.speed)
|
|
240
|
+
stats.speedDesc = `${(0, helper_1.formatByteSize)(stats.speed)}/s`;
|
|
238
241
|
stats.progress = Math.floor((finished / m3u8Info.tsCount) * 100);
|
|
239
242
|
if (downloadedDuration) {
|
|
240
243
|
stats.remainingTime = (timeCost / downloadedDuration) * (m3u8Info.duration - stats.durationDownloaded);
|
|
241
244
|
if (stats.speed > stats.avgSpeed)
|
|
242
245
|
stats.remainingTime = stats.remainingTime * (stats.avgSpeed / stats.speed);
|
|
243
246
|
stats.remainingTime = Math.ceil(stats.remainingTime);
|
|
244
|
-
stats.size = stats.downloadedSize * (stats.duration / stats.durationDownloaded);
|
|
247
|
+
stats.size = Math.round(stats.downloadedSize * (stats.duration / stats.durationDownloaded));
|
|
245
248
|
}
|
|
246
249
|
if (options.showProgress) {
|
|
247
250
|
const processBar = '='.repeat(Math.floor(stats.progress * 0.2)).padEnd(20, '-');
|
|
248
|
-
utils_js_1.logger.logInline(`${stats.progress}% [${(0, console_log_colors_1.greenBright)(processBar)}] ${(0, console_log_colors_1.cyan)(finished)} ${(0, console_log_colors_1.green)(stats.durationDownloaded.toFixed(2)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
?
|
|
252
|
-
:
|
|
253
|
-
? `${(0, console_log_colors_1.cyan)((0, fe_utils_1.formatTimeCost)(Date.now() - stats.remainingTime))}`
|
|
254
|
-
: ''));
|
|
251
|
+
utils_js_1.logger.logInline(`${stats.progress}% [${(0, console_log_colors_1.greenBright)(processBar)}] ${(0, console_log_colors_1.cyan)(finished)} ${(0, console_log_colors_1.green)(`${stats.durationDownloaded.toFixed(2)}sec`)} ${(0, console_log_colors_1.blueBright)((0, helper_1.formatByteSize)(stats.downloadedSize))} ${(0, console_log_colors_1.yellowBright)((0, fe_utils_1.formatTimeCost)(startTime))} ${(0, console_log_colors_1.magentaBright)(stats.speedDesc)} ${finished === m3u8Info.tsCount
|
|
252
|
+
? '\n'
|
|
253
|
+
: stats.remainingTime
|
|
254
|
+
? `${(0, console_log_colors_1.cyan)((0, fe_utils_1.formatTimeCost)(Date.now() - stats.remainingTime))}`
|
|
255
|
+
: ''}`);
|
|
255
256
|
}
|
|
256
257
|
if (options.onProgress)
|
|
257
258
|
options.onProgress(finished, m3u8Info.tsCount, info, stats);
|
|
@@ -265,23 +266,25 @@ async function m3u8Download(url, options = {}) {
|
|
|
265
266
|
}
|
|
266
267
|
};
|
|
267
268
|
if (options.showProgress) {
|
|
268
|
-
console.info(`\nTotal segments: ${(0, console_log_colors_1.cyan)(m3u8Info.tsCount)}, duration: ${(0, console_log_colors_1.green)(m3u8Info.duration
|
|
269
|
+
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)}`);
|
|
269
270
|
}
|
|
270
271
|
result.stats = stats;
|
|
271
|
-
(0, local_play_js_1.toLocalM3u8)(m3u8Info.data
|
|
272
|
+
(0, local_play_js_1.toLocalM3u8)(m3u8Info.data);
|
|
272
273
|
if (options.onInited)
|
|
273
274
|
options.onInited(stats, m3u8Info, workPoll);
|
|
274
275
|
runTask(m3u8Info.data);
|
|
275
276
|
await barrier.wait();
|
|
276
|
-
if (stats.tsFailed
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
277
|
+
if (stats.tsFailed > 0) {
|
|
278
|
+
utils_js_1.logger.warn('Download Failed! Please retry!', stats.tsFailed);
|
|
279
|
+
}
|
|
280
|
+
else if (options.convert !== false) {
|
|
281
|
+
result.filepath = await (0, m3u8_convert_js_1.m3u8Convert)(options, m3u8Info.data);
|
|
282
|
+
if (result.filepath && (0, node_fs_1.existsSync)(result.filepath)) {
|
|
283
|
+
stats.size = (0, node_fs_1.statSync)(result.filepath).size;
|
|
284
|
+
if (options.delCache)
|
|
280
285
|
(0, fe_utils_1.rmrfAsync)((0, node_path_1.dirname)(m3u8Info.data[0].tsOut));
|
|
281
286
|
}
|
|
282
287
|
}
|
|
283
|
-
else
|
|
284
|
-
utils_js_1.logger.warn('Download Failed! Please retry!', stats.tsFailed);
|
|
285
288
|
}
|
|
286
289
|
utils_js_1.logger.debug('Done!', url, result.m3u8Info);
|
|
287
290
|
return result;
|
package/cjs/lib/parseM3u8.d.ts
CHANGED
package/cjs/lib/parseM3u8.js
CHANGED
|
@@ -30,7 +30,14 @@ async function parseM3U8(content, cacheDir = './cache', headers) {
|
|
|
30
30
|
parser.end();
|
|
31
31
|
utils_1.logger.debug('parser.manifest', parser.manifest);
|
|
32
32
|
if (parser.manifest.playlists?.length > 0) {
|
|
33
|
-
|
|
33
|
+
let maxBandwidthItem = parser.manifest.playlists[0];
|
|
34
|
+
for (const item of parser.manifest.playlists) {
|
|
35
|
+
if (!maxBandwidthItem || (item.attributes?.BANDWIDTH || 0) > (maxBandwidthItem.attributes?.BANDWIDTH || 0)) {
|
|
36
|
+
maxBandwidthItem = item;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
url = new URL(maxBandwidthItem.uri, url).toString();
|
|
40
|
+
utils_1.logger.debug('maxBandwidthItem', maxBandwidthItem, url);
|
|
34
41
|
content = (await (0, utils_1.getRetry)(url, headers)).data;
|
|
35
42
|
parser = new m3u8_parser_1.Parser();
|
|
36
43
|
parser.push(content);
|
|
@@ -45,42 +52,44 @@ async function parseM3U8(content, cacheDir = './cache', headers) {
|
|
|
45
52
|
duration: 0,
|
|
46
53
|
data: [],
|
|
47
54
|
/** 加密相关信息 */
|
|
48
|
-
crypto: {
|
|
49
|
-
method: 'AES-128',
|
|
50
|
-
iv: new Uint8Array(16),
|
|
51
|
-
key: '',
|
|
52
|
-
uri: '',
|
|
53
|
-
},
|
|
55
|
+
crypto: {},
|
|
54
56
|
};
|
|
55
57
|
if (!result.tsCount) {
|
|
56
58
|
utils_1.logger.error('m3u8 file error!\n', url, content);
|
|
57
59
|
return result;
|
|
58
60
|
}
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
for (const [i, item] of tsList.entries()) {
|
|
62
|
+
if (!item.uri.includes('://'))
|
|
63
|
+
item.uri = new URL(item.uri, url).toString();
|
|
64
|
+
if (item.key) {
|
|
65
|
+
const tsKeyInfo = item.key;
|
|
66
|
+
if (!tsKeyInfo.uri.includes('://'))
|
|
67
|
+
tsKeyInfo.uri = new URL(tsKeyInfo.uri, url).toString();
|
|
68
|
+
if (tsKeyInfo?.uri && !result.crypto[tsKeyInfo.uri]) {
|
|
69
|
+
const r = await (0, utils_1.getRetry)(tsKeyInfo.uri);
|
|
70
|
+
if (r.response.statusCode !== 200) {
|
|
71
|
+
utils_1.logger.error('获取加密 key 失败:', tsKeyInfo.uri, r.response.statusCode, r.data);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
result.crypto[tsKeyInfo.uri] = {
|
|
75
|
+
uri: tsKeyInfo.uri,
|
|
76
|
+
method: tsKeyInfo.method.toUpperCase() || 'AES-128',
|
|
77
|
+
iv: typeof tsKeyInfo.iv === 'string' ? new Uint8Array(Buffer.from(tsKeyInfo.iv)) : tsKeyInfo.iv,
|
|
78
|
+
key: r.buffer,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
65
82
|
}
|
|
66
|
-
result.crypto.uri = tsKeyInfo.uri.includes('://') ? tsKeyInfo.uri : new URL(tsKeyInfo.uri, url).toString();
|
|
67
|
-
}
|
|
68
|
-
if (result.crypto.uri !== '') {
|
|
69
|
-
const r = await (0, utils_1.getRetry)(result.crypto.uri);
|
|
70
|
-
result.crypto.key = r.buffer;
|
|
71
|
-
}
|
|
72
|
-
for (let i = 0; i < result.tsCount; i++) {
|
|
73
|
-
if (!tsList[i].uri.startsWith('http'))
|
|
74
|
-
tsList[i].uri = new URL(tsList[i].uri, url).toString();
|
|
75
83
|
result.data.push({
|
|
76
|
-
m3u8: url,
|
|
77
84
|
index: i,
|
|
78
|
-
duration:
|
|
79
|
-
timeline:
|
|
80
|
-
uri:
|
|
81
|
-
tsOut: (0, node_path_1.resolve)(cacheDir, `${(0, fe_utils_1.md5)(
|
|
85
|
+
duration: item.duration,
|
|
86
|
+
timeline: item.timeline,
|
|
87
|
+
uri: item.uri,
|
|
88
|
+
tsOut: (0, node_path_1.resolve)(cacheDir, `${(0, fe_utils_1.md5)(item.uri)}.ts`),
|
|
89
|
+
keyUri: item.key?.uri || '',
|
|
90
|
+
m3u8: url,
|
|
82
91
|
});
|
|
83
|
-
result.duration +=
|
|
92
|
+
result.duration += item.duration;
|
|
84
93
|
}
|
|
85
94
|
result.duration = +Number(result.duration).toFixed(2);
|
|
86
95
|
return result;
|
|
@@ -41,7 +41,7 @@ class CommSearchApi {
|
|
|
41
41
|
}
|
|
42
42
|
async search(wd, api = this.options.api) {
|
|
43
43
|
let { data } = await req.get(api, { wd }, null, { rejectUnauthorized: false });
|
|
44
|
-
if (typeof data
|
|
44
|
+
if (typeof data === 'string')
|
|
45
45
|
data = JSON.parse(data);
|
|
46
46
|
return data;
|
|
47
47
|
}
|
|
@@ -54,7 +54,7 @@ class CommSearchApi {
|
|
|
54
54
|
ac: 'videolist',
|
|
55
55
|
ids: Array.isArray(ids) ? ids.join(',') : ids,
|
|
56
56
|
}, null, { rejectUnauthorized: false });
|
|
57
|
-
if (typeof data
|
|
57
|
+
if (typeof data === 'string')
|
|
58
58
|
data = JSON.parse(data);
|
|
59
59
|
return data;
|
|
60
60
|
}
|
package/cjs/lib/storage.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.stor = void 0;
|
|
4
|
-
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
5
|
-
const node_path_1 = require("node:path");
|
|
6
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
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,3 +1,3 @@
|
|
|
1
|
-
import type { M3u8Crypto, TsItemInfo } from '../types/m3u8';
|
|
2
1
|
import type { IncomingHttpHeaders } from 'node:http';
|
|
2
|
+
import type { M3u8Crypto, TsItemInfo } from '../types/m3u8';
|
|
3
3
|
export declare function tsDownload(info: TsItemInfo, cryptoInfo: M3u8Crypto, headers?: IncomingHttpHeaders): Promise<boolean>;
|
package/cjs/lib/ts-download.js
CHANGED
|
@@ -14,7 +14,7 @@ async function tsDownload(info, cryptoInfo, headers) {
|
|
|
14
14
|
const r = await (0, utils_js_1.getRetry)(info.uri, headers);
|
|
15
15
|
if (r.response.statusCode === 200) {
|
|
16
16
|
utils_js_1.logger.debug('\n', info);
|
|
17
|
-
const data = cryptoInfo
|
|
17
|
+
const data = cryptoInfo?.key ? aesDecrypt(r.buffer, cryptoInfo) : r.buffer;
|
|
18
18
|
(0, fe_utils_1.mkdirp)((0, node_path_1.dirname)(info.tsOut));
|
|
19
19
|
await node_fs_1.promises.writeFile(info.tsOut, data);
|
|
20
20
|
info.tsSize = r.buffer.byteLength;
|
|
@@ -29,7 +29,8 @@ async function tsDownload(info, cryptoInfo, headers) {
|
|
|
29
29
|
}
|
|
30
30
|
function aesDecrypt(data, cryptoInfo) {
|
|
31
31
|
try {
|
|
32
|
-
const
|
|
32
|
+
const iv = cryptoInfo.iv || new Uint8Array(16);
|
|
33
|
+
const decipher = (0, node_crypto_1.createDecipheriv)(`${cryptoInfo.method}-cbc`.toLocaleLowerCase(), cryptoInfo.key, iv);
|
|
33
34
|
return Buffer.concat([decipher.update(Buffer.isBuffer(data) ? data : Buffer.from(data)), decipher.final()]);
|
|
34
35
|
}
|
|
35
36
|
catch (err) {
|
package/cjs/lib/utils.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { type Stats } from 'node:fs';
|
|
1
2
|
import type { IncomingHttpHeaders } from 'node:http';
|
|
2
|
-
import { Stats } from 'node:fs';
|
|
3
3
|
import { NLogger, Request } from '@lzwme/fe-utils';
|
|
4
4
|
export declare const request: Request;
|
|
5
5
|
export declare const getRetry: <T = string>(url: string, headers?: IncomingHttpHeaders, retries?: number) => Promise<{
|
package/cjs/lib/utils.js
CHANGED
|
@@ -38,9 +38,9 @@ function findFiles(apidir, validate) {
|
|
|
38
38
|
files.push((0, node_path_1.resolve)(apidir));
|
|
39
39
|
}
|
|
40
40
|
else if (stat.isDirectory()) {
|
|
41
|
-
(0, node_fs_1.readdirSync)(apidir)
|
|
42
|
-
findFiles((0, node_path_1.resolve)(apidir, filename))
|
|
43
|
-
}
|
|
41
|
+
for (const filename of (0, node_fs_1.readdirSync)(apidir)) {
|
|
42
|
+
files.push(...findFiles((0, node_path_1.resolve)(apidir, filename)));
|
|
43
|
+
}
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -49,7 +49,7 @@ function findFiles(apidir, validate) {
|
|
|
49
49
|
/** 获取重定向后的 URL */
|
|
50
50
|
async function getLocation(url, method = 'HEAD') {
|
|
51
51
|
const { res } = await exports.request.req(url, null, { method, headers: { 'content-type': 'text/html' } }, false);
|
|
52
|
-
const rurl = res.headers
|
|
52
|
+
const rurl = res.headers.location || res.headers['x-redirect'] || res.headers['x-location'];
|
|
53
53
|
if (typeof rurl === 'string' && rurl !== url)
|
|
54
54
|
return getLocation(rurl, method);
|
|
55
55
|
return url;
|