@lzwme/m3u8-dl 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 renxia<lzwy0820@qq.com, https://lzw.me>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.MD ADDED
@@ -0,0 +1,140 @@
1
+ [![@lzwme/m3u8-dl](https://nodei.co/npm/@lzwme/m3u8-dl.png)][npm-url]
2
+
3
+ # @lzwme/m3u8-dl
4
+
5
+ [![NPM version][npm-badge]][npm-url]
6
+ [![node version][node-badge]][node-url]
7
+ ![license MIT](https://img.shields.io/github/license/lzwme/m3u8-dl)
8
+
9
+ [![build status](https://github.com/lzwme/m3u8-dl/actions/workflows/node-ci.yml/badge.svg)](https://github.com/lzwme/m3u8-dl/actions/workflows/node-ci.yml)
10
+ [![npm download][download-badge]][download-url]
11
+ [![GitHub issues][issues-badge]][issues-url]
12
+ [![GitHub forks][forks-badge]][forks-url]
13
+ [![GitHub stars][stars-badge]][stars-url]
14
+
15
+ 一个 m3u8 文件批量下载工具。
16
+
17
+ ## 功能特性(Features)
18
+
19
+ - 多线程下载。线程池模式的多线程下载。
20
+ - `边下边播模式`。支持使用已下载的 ts 缓存文件在线播放。
21
+ - 支持指定多个 m3u8 地址批量下载。
22
+ - 支持缓存续传。下载失败则保留缓存,重试时只下载失败的片段。
23
+ - 支持常见的 AES 解密。
24
+ - 支持转换为 mp4。**需全局安装 ffmpeg**
25
+
26
+ ## 安装(Install)
27
+
28
+ ```bash
29
+ npm i -g @lzwme/m3u8-dl
30
+ m3u8dl -h
31
+ ```
32
+
33
+ 或者使用 `npx`:
34
+
35
+ ```bash
36
+ npx @lzwme/m3u8-dl -h
37
+ ```
38
+
39
+ ## 使用(Useage)
40
+
41
+ 提示:如需要下载并转换为 `mp4` 视频格式,您需全局安装 [ffmpeg](https://ffmpeg.org/download.html)。
42
+
43
+ ### 命令行方式(Command Line Interface)
44
+
45
+ ```bash
46
+ m3u8dl --help
47
+ ```
48
+
49
+ **下载指定 URL 的 m3u8 文件:**
50
+
51
+ ```bash
52
+ m3u8dl https://lzw.me/x/m3u8-player/test.m3u8
53
+ ```
54
+
55
+ **批量下载示例一:**
56
+
57
+ ```bash
58
+ # 下载多个文件:
59
+ m3u8dl "第1集|https://s.xlzys.com/play/zbqMZYRb/index.m3u8" "第2集|https://s.xlzys.com/play/PdyJXrwe/index.m3u8" --filename "三体"
60
+ ```
61
+
62
+ **批量下载示例二:**
63
+
64
+ 新建文件 `三体.txt`,内容格式:
65
+
66
+ ```txt
67
+ 第1集$https://s.xlzys.com/play/zbqMZYRb/index.m3u8
68
+ 第2集$https://s.xlzys.com/play/PdyJXrwe/index.m3u8
69
+ 第3集$https://s.xlzys.com/play/oeE6x9Ka/index.m3u8
70
+ ```
71
+
72
+ 然后执行如下命令:
73
+
74
+ ```bash
75
+ m3u8dl 三体.txt
76
+ ```
77
+
78
+ 提示:可创建并指定多个 txt 文件实现对多个影视剧集的一键批量下载。
79
+
80
+ ### API 调用
81
+
82
+ ```ts
83
+ import { m3u8Download } from '@lzwme/m3u8-dl';
84
+
85
+ // 示例:单文件下载
86
+ m3u8Download('test/t.m3u8', { debug: true, filenmae: '测试视频' });
87
+
88
+ // 示例:批量下载
89
+ const fileList = ['第一集$$test/t.m3u8'];
90
+ for (const filepath of fileList) {
91
+ const r = await m3u8Download(filepath, { debug: true, filenmae: '测试视频' });
92
+ console.log('文件已下载:', r.filepath);
93
+ }
94
+ ```
95
+
96
+ ## API 文档
97
+
98
+ - [https://lzwme.github.io/m3u8-dl/](https://lzwme.github.io/m3u8-dl/)
99
+
100
+ ## 开发(Development)
101
+
102
+ 本地二次开发:
103
+
104
+ ```bash
105
+ git clone git@github.com:lzwme/m3u8-dl.git
106
+ pnpm install
107
+ pnpm dev
108
+ # npm link
109
+ ```
110
+
111
+ 或者 [fork](https://github.com/lzwme/m3u8-dl/fork) 本项目进行代码贡献。
112
+
113
+ **欢迎贡献想法与代码。**
114
+
115
+ ## References
116
+
117
+ - [ffmpeg download](https://ffmpeg.org/download.html)
118
+ - [m3u8-multi-thread-downloader](https://github.com/sahadev/m3u8Downloader)
119
+ - [m3u8Utils](https://github.com/liupishui/m3u8Utils)
120
+
121
+ ## License
122
+
123
+ `@lzwme/m3u8-dl` is released under the MIT license.
124
+
125
+ 该插件由[志文工作室](https://lzw.me)开发和维护。
126
+
127
+ [stars-badge]: https://img.shields.io/github/stars/lzwme/m3u8-dl.svg
128
+ [stars-url]: https://github.com/lzwme/m3u8-dl/stargazers
129
+ [forks-badge]: https://img.shields.io/github/forks/lzwme/m3u8-dl.svg
130
+ [forks-url]: https://github.com/lzwme/m3u8-dl/network
131
+ [issues-badge]: https://img.shields.io/github/issues/lzwme/m3u8-dl.svg
132
+ [issues-url]: https://github.com/lzwme/m3u8-dl/issues
133
+ [npm-badge]: https://img.shields.io/npm/v/@lzwme/m3u8-dl.svg?style=flat-square
134
+ [npm-url]: https://npmjs.com/package/@lzwme/m3u8-dl
135
+ [node-badge]: https://img.shields.io/badge/node.js-%3E=_14.18.0-green.svg?style=flat-square
136
+ [node-url]: https://nodejs.org/download/
137
+ [download-badge]: https://img.shields.io/npm/dm/@lzwme/m3u8-dl.svg?style=flat-square
138
+ [download-url]: https://npmjs.com/package/@lzwme/m3u8-dl
139
+ [bundlephobia-url]: https://bundlephobia.com/result?p=@lzwme/m3u8-dl@latest
140
+ [bundlephobia-badge]: https://badgen.net/bundlephobia/minzip/@lzwme/m3u8-dl@latest
package/bin/m3u8dl.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ require('../cjs/cli.js');
package/cjs/cli.js ADDED
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const node_path_1 = require("node:path");
4
+ const commander_1 = require("commander");
5
+ const console_log_colors_1 = require("console-log-colors");
6
+ const fe_utils_1 = require("@lzwme/fe-utils");
7
+ const utils_js_1 = require("./lib/utils.js");
8
+ const m3u8_batch_download_1 = require("./m3u8-batch-download");
9
+ const pkg = (0, fe_utils_1.readJsonFileSync)((0, node_path_1.resolve)(__dirname, '../package.json'));
10
+ commander_1.program
11
+ .version(pkg.version, '-v, --version')
12
+ .description((0, console_log_colors_1.cyanBright)(pkg.description))
13
+ .argument('<m3u8Urls...>', 'm3u8 url。也可以是本地 txt 文件,指定一组 m3u8,适用于批量下载的场景')
14
+ .option('--silent', `开启静默模式。`)
15
+ .option('--debug', `开启调试模式。`)
16
+ .option('-F,--filename <name>', `指定下载文件的保存名称。默认取 url md5 值。若指定了多个 url 地址,则会在末尾增加序号`)
17
+ .option('-n,--thread-num <number>', `并发下载线程数。取决于服务器限制,过多可能会容易下载失败。一般建议不超过 4 个`, '4')
18
+ .option('-f,--force', `文件已存在时,是否仍继续下载和生成`)
19
+ .option('--no-progress', `是否不打印进度信息`)
20
+ .option('--play', `是否边下边看`)
21
+ .option('--cache-dir <dirpath>', `临时文件保存目录。默认为 cache`)
22
+ .option('--save-dir <dirpath>', `下载文件保存的路径。默认为当前目录`)
23
+ .option('--no-del-cache', `下载成功后是否删除临时文件。默认为 true。保存临时文件可以在重复下载时识别缓存`, true)
24
+ .action(async (urls, options) => {
25
+ if (options.debug)
26
+ utils_js_1.logger.updateOptions({ levelType: 'debug' });
27
+ else if (options.silent)
28
+ utils_js_1.logger.updateOptions({ levelType: 'silent' });
29
+ utils_js_1.logger.debug(urls, options);
30
+ if (options.progress != null)
31
+ options.showProgress = options.progress;
32
+ if (urls.length > 0) {
33
+ await (0, m3u8_batch_download_1.m3u8BatchDownload)(urls, options);
34
+ }
35
+ else
36
+ commander_1.program.help();
37
+ });
38
+ commander_1.program.parse(process.argv);
package/cjs/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './lib/m3u8-download';
2
+ export * from './lib/parseM3u8';
package/cjs/index.js ADDED
@@ -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("./lib/m3u8-download"), exports);
18
+ __exportStar(require("./lib/parseM3u8"), exports);
@@ -0,0 +1,11 @@
1
+ /// <reference types="node" />
2
+ import { M3u8DLOptions, TsItemInfo } from '../type';
3
+ /**
4
+ * 边下边看
5
+ */
6
+ export declare function localPlay(m3u8Info: TsItemInfo[], _options: M3u8DLOptions): Promise<{
7
+ port: number;
8
+ origin: string;
9
+ server: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
10
+ }>;
11
+ export declare function toLocalM3u8(m3u8Info: TsItemInfo[], filepath: string, host?: string): Promise<void>;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toLocalM3u8 = exports.localPlay = void 0;
4
+ const fe_utils_1 = require("@lzwme/fe-utils");
5
+ const console_log_colors_1 = require("console-log-colors");
6
+ const node_fs_1 = require("node:fs");
7
+ const node_http_1 = require("node:http");
8
+ const node_path_1 = require("node:path");
9
+ const utils_1 = require("./utils");
10
+ /**
11
+ * 边下边看
12
+ */
13
+ async function localPlay(m3u8Info, _options) {
14
+ if (!m3u8Info?.length)
15
+ return null;
16
+ const cacheDir = (0, node_path_1.dirname)(m3u8Info[0].tsOut);
17
+ const info = await createLocalServer(cacheDir);
18
+ const filename = (0, fe_utils_1.md5)(cacheDir).slice(0, 8) + `.m3u8`;
19
+ await toLocalM3u8(m3u8Info, (0, node_path_1.resolve)(cacheDir, filename), info.origin);
20
+ const playUrl = `https://lzw.me/x/m3u8-player?url=${encodeURIComponent(`${info.origin}/${filename}`)}`;
21
+ const cmd = `${process.platform === 'win32' ? 'start' : 'open'} ${playUrl}`;
22
+ (0, fe_utils_1.execSync)(cmd);
23
+ return info;
24
+ }
25
+ exports.localPlay = localPlay;
26
+ async function toLocalM3u8(m3u8Info, filepath, host = '') {
27
+ const m3u8ContentList = [
28
+ `#EXTM3U`,
29
+ `#EXT-X-VERSION:3`,
30
+ `#EXT-X-ALLOW-CACHE:YES`,
31
+ `#EXT-X-TARGETDURATION:${Math.max(...m3u8Info.map(d => d.duration))}`,
32
+ `#EXT-X-MEDIA-SEQUENCE:0`,
33
+ // `#EXT-X-KEY:METHOD=AES-128,URI="/api/aes/enc.key"`,
34
+ ];
35
+ m3u8Info.forEach(d => {
36
+ if (d.tsOut)
37
+ m3u8ContentList.push(`#EXTINF:${Number(d.duration).toFixed(6)},`, `${host}/${(0, node_path_1.basename)(d.tsOut)}`);
38
+ });
39
+ m3u8ContentList.push(`#EXT-X-ENDLIST`);
40
+ const m3u8Content = m3u8ContentList.join('\n');
41
+ await node_fs_1.promises.writeFile(filepath, m3u8Content, 'utf8');
42
+ }
43
+ exports.toLocalM3u8 = toLocalM3u8;
44
+ async function createLocalServer(baseDir) {
45
+ const port = await (0, fe_utils_1.findFreePort)();
46
+ const origin = `http://localhost:${port}`;
47
+ const server = (0, node_http_1.createServer)((req, res) => {
48
+ const filename = (0, node_path_1.join)(baseDir, req.url);
49
+ utils_1.logger.debug('[req]', req.url, filename);
50
+ if ((0, node_fs_1.existsSync)(filename)) {
51
+ const stats = (0, node_fs_1.statSync)(filename);
52
+ const ext = (0, node_path_1.extname)(filename);
53
+ if (stats.isFile()) {
54
+ if (ext === '.m3u8')
55
+ res.setHeader('Cache-Control', 'no-cache');
56
+ res.writeHead(200, {
57
+ 'Last-Modified': stats.mtime.toUTCString(),
58
+ 'Access-Control-Allow-Headers': '*',
59
+ 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
60
+ 'Access-Control-Allow-Origin': '*',
61
+ 'Content-Length': String(stats.size),
62
+ 'Content-Type': ext === '.ts' ? 'video/mp2t' : ext === '.m3u8' ? 'application/vnd.apple.mpegurl' : 'text/plain',
63
+ });
64
+ (0, node_fs_1.createReadStream)(filename).pipe(res);
65
+ return;
66
+ }
67
+ }
68
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
69
+ res.end('Not found');
70
+ }).listen(port, () => {
71
+ utils_1.logger.info('Created Local Server:', console_log_colors_1.color.greenBright(origin));
72
+ });
73
+ return { port, origin, server };
74
+ }
@@ -0,0 +1,2 @@
1
+ import type { M3u8DLOptions, TsItemInfo } from '../type';
2
+ export declare function m3u8Convert(options: M3u8DLOptions, data: TsItemInfo[]): Promise<string>;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.m3u8Convert = void 0;
4
+ const node_fs_1 = require("node:fs");
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
+ const utils_1 = require("./utils");
9
+ async function m3u8Convert(options, data) {
10
+ let ffmpegSupport = (0, utils_1.isSupportFfmpeg)();
11
+ let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename);
12
+ if (!options.force && (0, node_fs_1.existsSync)(filepath))
13
+ return filepath;
14
+ if (ffmpegSupport) {
15
+ const inputFilePath = (0, node_path_1.resolve)(options.cacheDir, 'input.txt');
16
+ const filesAllArr = data.map(d => (0, node_path_1.resolve)(d.tsOut)).filter(d => (0, node_fs_1.existsSync)(d));
17
+ await node_fs_1.promises.writeFile(inputFilePath, 'ffconcat version 1.0\nfile ' + filesAllArr.join('\nfile '));
18
+ const cmd = `ffmpeg -y -f concat -safe 0 -i ${inputFilePath} -acodec copy -vcodec copy -absf aac_adtstoasc ${filepath}`;
19
+ utils_1.logger.debug('[m3u8-to-mp4]cmd:', (0, console_log_colors_1.cyan)(cmd));
20
+ const r = (0, fe_utils_1.execSync)(cmd);
21
+ ffmpegSupport = !r.error;
22
+ if (r.error)
23
+ utils_1.logger.error('Conversion to mp4 failed. Please confirm that ffmpeg is installed!', r.stderr);
24
+ }
25
+ if (!ffmpegSupport) {
26
+ filepath = filepath.replace('.mp4', '.ts');
27
+ if (!options.force && (0, node_fs_1.existsSync)(filepath))
28
+ return filepath;
29
+ const buf = Buffer.concat(data.map(d => (0, node_fs_1.readFileSync)(d.tsOut)));
30
+ await node_fs_1.promises.writeFile(filepath, buf);
31
+ }
32
+ utils_1.logger.info(ffmpegSupport ? 'Generated mp4 file:' : 'Merged into ts file:', (0, console_log_colors_1.greenBright)(filepath));
33
+ return filepath;
34
+ }
35
+ exports.m3u8Convert = m3u8Convert;
@@ -0,0 +1,12 @@
1
+ import type { M3u8DLOptions, TsItemInfo } from '../type';
2
+ export declare function m3u8Download(url: string, options?: M3u8DLOptions): Promise<{
3
+ options: M3u8DLOptions;
4
+ m3u8Info: {
5
+ manifest: any;
6
+ tsCount: number;
7
+ durationSecond: number;
8
+ data: TsItemInfo[];
9
+ crypto: import("../type").M3u8Crypto;
10
+ };
11
+ filepath: string;
12
+ }>;
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.m3u8Download = void 0;
4
+ const node_path_1 = require("node:path");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_os_1 = require("node:os");
7
+ const fe_utils_1 = require("@lzwme/fe-utils");
8
+ const console_log_colors_1 = require("console-log-colors");
9
+ const utils_1 = require("./utils");
10
+ const worker_pool_1 = require("./worker_pool");
11
+ const parseM3u8_1 = require("./parseM3u8");
12
+ const m3u8_convert_1 = require("./m3u8-convert");
13
+ const local_play_1 = require("./local-play");
14
+ const tsDlFile = (0, node_path_1.resolve)(__dirname, './ts-download.js');
15
+ async function formatOptions(url, opts) {
16
+ const options = {
17
+ delCache: !opts.debug,
18
+ saveDir: process.cwd(),
19
+ ...opts,
20
+ };
21
+ if (!url.startsWith('http')) {
22
+ url = url.replace('$', '|').replace(/\|\|+/, '|');
23
+ if (url.includes('|')) {
24
+ const r = url.split('|');
25
+ url = r[1];
26
+ if (!options.filename)
27
+ options.filename = r[0];
28
+ else
29
+ options.filename = `${options.filename.replace(/\.(ts|mp4)$/, '')}-${r[0]}`;
30
+ }
31
+ }
32
+ const urlMd5 = (0, fe_utils_1.md5)(url, false);
33
+ if (+options.threadNum <= 0)
34
+ options.threadNum = (0, node_os_1.cpus)().length;
35
+ if (!options.filename)
36
+ options.filename = urlMd5;
37
+ if (!options.filename.endsWith('.mp4'))
38
+ options.filename += '.mp4';
39
+ if (!options.cacheDir)
40
+ options.cacheDir = `cache/${urlMd5}`;
41
+ if (!(0, node_fs_1.existsSync)(options.cacheDir))
42
+ await node_fs_1.promises.mkdir(options.cacheDir, { recursive: true });
43
+ if (options.headers)
44
+ utils_1.request.setHeaders(options.headers);
45
+ if (options.debug) {
46
+ utils_1.logger.updateOptions({ levelType: 'debug' });
47
+ utils_1.logger.debug('[m3u8-DL]options', options);
48
+ }
49
+ return [url, options];
50
+ }
51
+ async function m3u8Download(url, options = {}) {
52
+ utils_1.logger.info('starting download for', (0, console_log_colors_1.cyanBright)(url));
53
+ [url, options] = await formatOptions(url, options);
54
+ const ext = (0, utils_1.isSupportFfmpeg)() ? '.mp4' : '.ts';
55
+ let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename);
56
+ if (!filepath.endsWith(ext))
57
+ filepath += ext;
58
+ const m3u8Info = await (0, parseM3u8_1.parseM3U8)('', url, options.cacheDir);
59
+ const result = { options, m3u8Info, filepath };
60
+ if (!options.force && (0, node_fs_1.existsSync)(filepath)) {
61
+ utils_1.logger.info('file already exist:', filepath);
62
+ return result;
63
+ }
64
+ if (m3u8Info.tsCount > 0) {
65
+ const pool = new worker_pool_1.WorkerPool(tsDlFile, options.threadNum);
66
+ const barrier = new fe_utils_1.Barrier();
67
+ const playStart = Math.min(options.threadNum + 2, m3u8Info.tsCount);
68
+ const stats = {
69
+ /** 下载成功的 ts 数量 */
70
+ tsSuccess: 0,
71
+ /** 下载失败的 ts 数量 */
72
+ tsFailed: 0,
73
+ /** 下载完成的时长 */
74
+ duration: 0,
75
+ };
76
+ for (const info of m3u8Info.data) {
77
+ pool.runTask({ info, options: JSON.parse(JSON.stringify(options)), crypto: m3u8Info.crypto }, (err, res) => {
78
+ if (err) {
79
+ console.log('\n');
80
+ utils_1.logger.error('[TS-DL][error]', info.index, err, res);
81
+ }
82
+ if (!res)
83
+ return;
84
+ if (res.success) {
85
+ stats.tsSuccess++;
86
+ stats.duration = +(stats.duration + info.duration).toFixed(2);
87
+ }
88
+ else {
89
+ stats.tsFailed++;
90
+ }
91
+ info.success = res.success;
92
+ const finished = stats.tsFailed + stats.tsSuccess;
93
+ if (options.showProgress !== false) {
94
+ const percent = Math.ceil((finished / m3u8Info.tsCount) * 100);
95
+ const processBar = '='.repeat(Math.ceil(percent * 0.4)).padEnd(40, '-');
96
+ utils_1.logger.logInline(`Downloading: ${percent}% [${(0, console_log_colors_1.green)(processBar)}] segment: ${finished}/${m3u8Info.tsCount}, duration: ${stats.duration}/${m3u8Info.durationSecond}sec${finished === m3u8Info.tsCount ? '\n' : ''}`);
97
+ }
98
+ if (options.onProgress)
99
+ options.onProgress(finished, m3u8Info.tsCount, info);
100
+ if (finished === m3u8Info.tsCount) {
101
+ pool.close();
102
+ barrier.open();
103
+ }
104
+ if (options.play && finished === playStart) {
105
+ (0, local_play_1.localPlay)(m3u8Info.data, options);
106
+ }
107
+ });
108
+ }
109
+ await barrier.wait();
110
+ if (stats.tsFailed === 0) {
111
+ result.filepath = await (0, m3u8_convert_1.m3u8Convert)(options, m3u8Info.data);
112
+ if ((0, node_fs_1.existsSync)(options.cacheDir) && options.delCache)
113
+ (0, fe_utils_1.rmrf)(options.cacheDir);
114
+ }
115
+ else
116
+ utils_1.logger.debug('Download Failed! Please retry!', stats.tsFailed);
117
+ }
118
+ utils_1.logger.debug('Done!', url, result.m3u8Info);
119
+ return result;
120
+ }
121
+ exports.m3u8Download = m3u8Download;
@@ -0,0 +1,11 @@
1
+ import type { M3u8Crypto, TsItemInfo } from '../type';
2
+ export declare function parseM3U8(content: string, url?: string, cacheDir?: string): Promise<{
3
+ manifest: any;
4
+ /** ts 文件数量 */
5
+ tsCount: number;
6
+ /** 总时长 */
7
+ durationSecond: number;
8
+ data: TsItemInfo[];
9
+ /** 加密相关信息 */
10
+ crypto: M3u8Crypto;
11
+ }>;
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseM3U8 = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const m3u8_parser_1 = require("m3u8-parser");
7
+ const utils_1 = require("./utils");
8
+ async function parseM3U8(content, url = process.cwd(), cacheDir = './cache') {
9
+ if (!content && url) {
10
+ if (!url.startsWith('http') && (0, node_fs_1.existsSync)(url)) {
11
+ url = (0, node_path_1.resolve)(process.cwd(), url);
12
+ content = await node_fs_1.promises.readFile(url, 'utf8');
13
+ }
14
+ else {
15
+ content = (await (0, utils_1.getRetry)(url)).data;
16
+ }
17
+ }
18
+ utils_1.logger.debug('starting parsing m3u8 file:', url);
19
+ let parser = new m3u8_parser_1.Parser();
20
+ parser.push(content);
21
+ parser.end();
22
+ if (parser.manifest.playlists?.length > 0) {
23
+ url = new URL(url, parser.manifest.playlists[0].uri).toString();
24
+ content = (await (0, utils_1.getRetry)(url)).data;
25
+ parser = new m3u8_parser_1.Parser();
26
+ parser.push(content);
27
+ parser.end();
28
+ }
29
+ const tsList = parser.manifest.segments || [];
30
+ const result = {
31
+ manifest: parser.manifest,
32
+ /** ts 文件数量 */
33
+ tsCount: tsList.length,
34
+ /** 总时长 */
35
+ durationSecond: 0,
36
+ data: [],
37
+ /** 加密相关信息 */
38
+ crypto: {
39
+ method: 'AES-128',
40
+ iv: new Uint8Array(16),
41
+ key: '',
42
+ uri: '',
43
+ },
44
+ };
45
+ if (!result.tsCount) {
46
+ utils_1.logger.error('m3u8 file error!\n', url, content);
47
+ return result;
48
+ }
49
+ const tsKeyInfo = tsList[0].key;
50
+ if (tsKeyInfo?.uri) {
51
+ if (tsKeyInfo.method)
52
+ result.crypto.method = tsKeyInfo.method.toUpperCase();
53
+ if (tsKeyInfo.iv)
54
+ result.crypto.iv = new Uint8Array(Buffer.from(tsKeyInfo.iv));
55
+ result.crypto.uri = tsKeyInfo.uri.includes('://') ? tsKeyInfo.uri : new URL(tsKeyInfo.uri, url).toString();
56
+ }
57
+ if (result.crypto.uri !== '') {
58
+ const r = await (0, utils_1.getRetry)(result.crypto.uri);
59
+ result.crypto.key = r.buffer;
60
+ }
61
+ for (let i = 0; i < result.tsCount; i++) {
62
+ if (!tsList[i].uri.startsWith('http'))
63
+ tsList[i].uri = new URL(tsList[i].uri, url).toString();
64
+ result.data.push({
65
+ index: i,
66
+ duration: tsList[i].duration,
67
+ timeline: tsList[i].timeline,
68
+ uri: tsList[i].uri,
69
+ tsOut: `${cacheDir}/${i}-${(0, node_path_1.basename)(tsList[i].uri).replace(/\.ts\?.+/, '.ts')}`,
70
+ });
71
+ result.durationSecond += tsList[i].duration;
72
+ }
73
+ return result;
74
+ }
75
+ exports.parseM3U8 = parseM3U8;
76
+ // parseM3U8('', 't.m3u8').then(d => console.log(d));
@@ -0,0 +1,2 @@
1
+ import type { M3u8Crypto, TsItemInfo } from '../type';
2
+ export declare function tsDownload(info: TsItemInfo, cryptoInfo: M3u8Crypto): Promise<boolean>;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tsDownload = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_worker_threads_1 = require("node:worker_threads");
7
+ const utils_1 = require("./utils");
8
+ async function tsDownload(info, cryptoInfo) {
9
+ try {
10
+ if ((0, node_fs_1.existsSync)(info.tsOut))
11
+ return true;
12
+ const r = await (0, utils_1.getRetry)(info.uri);
13
+ if (r.response.statusCode === 200) {
14
+ utils_1.logger.debug('\n', info);
15
+ const data = cryptoInfo.key ? aesDecrypt(r.buffer, cryptoInfo) : r.buffer;
16
+ await node_fs_1.promises.writeFile(info.tsOut, data);
17
+ return true;
18
+ }
19
+ utils_1.logger.warn('[TS-Download][failed]', r.response.statusCode, info.uri);
20
+ }
21
+ catch (e) {
22
+ utils_1.logger.error('[TS-Download][error]', e.message || e);
23
+ }
24
+ return false;
25
+ }
26
+ exports.tsDownload = tsDownload;
27
+ function aesDecrypt(data, cryptoInfo) {
28
+ const decipher = (0, node_crypto_1.createDecipheriv)((cryptoInfo.method + '-cbc').toLocaleLowerCase(), cryptoInfo.key, cryptoInfo.iv);
29
+ return Buffer.concat([decipher.update(Buffer.isBuffer(data) ? data : Buffer.from(data)), decipher.final()]);
30
+ }
31
+ if (!node_worker_threads_1.isMainThread && node_worker_threads_1.parentPort) {
32
+ node_worker_threads_1.parentPort.on('message', (data) => {
33
+ if (data.options?.headers)
34
+ utils_1.request.setHeaders(data.options.headers);
35
+ tsDownload(data.info, data.crypto).then(success => {
36
+ data.info.success = success;
37
+ node_worker_threads_1.parentPort.postMessage({ success, info: data.info });
38
+ });
39
+ });
40
+ }
@@ -0,0 +1,12 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ import { NLogger, Request } from '@lzwme/fe-utils';
4
+ export declare const request: Request;
5
+ export declare const getRetry: <T = string>(url: string, retries?: number) => Promise<{
6
+ data: T;
7
+ buffer: Buffer;
8
+ headers: import("http").IncomingHttpHeaders;
9
+ response: import("http").IncomingMessage;
10
+ }>;
11
+ export declare const logger: NLogger;
12
+ export declare function isSupportFfmpeg(): boolean;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isSupportFfmpeg = exports.logger = exports.getRetry = exports.request = void 0;
4
+ const fe_utils_1 = require("@lzwme/fe-utils");
5
+ exports.request = new fe_utils_1.Request('', {
6
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
7
+ });
8
+ const getRetry = (url, retries = 3) => (0, fe_utils_1.retry)(() => exports.request.get(url), 1000, retries, r => r.response.statusCode === 200);
9
+ exports.getRetry = getRetry;
10
+ exports.logger = fe_utils_1.NLogger.getLogger('[M3U8-DL]', { color: fe_utils_1.color });
11
+ let _isSupportFfmpeg = null;
12
+ function isSupportFfmpeg() {
13
+ if (null == _isSupportFfmpeg)
14
+ _isSupportFfmpeg = (0, fe_utils_1.execSync)('ffmpeg -version').stderr === '';
15
+ return _isSupportFfmpeg;
16
+ }
17
+ exports.isSupportFfmpeg = isSupportFfmpeg;
@@ -0,0 +1,14 @@
1
+ /// <reference types="node" />
2
+ import { EventEmitter } from 'node:events';
3
+ export declare class WorkerPool<T = unknown, R = unknown> extends EventEmitter {
4
+ private processorFile;
5
+ numThreads: number;
6
+ private workers;
7
+ private freeWorkers;
8
+ private workerTaskInfo;
9
+ private tasks;
10
+ constructor(processorFile: string, numThreads?: number);
11
+ addNewWorker(processorFile?: string): void;
12
+ runTask(task: T, callback: (err: Error | null, result: R) => void): void;
13
+ close(): void;
14
+ }
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WorkerPool = void 0;
4
+ const node_events_1 = require("node:events");
5
+ const node_async_hooks_1 = require("node:async_hooks");
6
+ const node_worker_threads_1 = require("node:worker_threads");
7
+ const node_os_1 = require("node:os");
8
+ const node_fs_1 = require("node:fs");
9
+ const kWorkerFreedEvent = Symbol('kWorkerFreedEvent');
10
+ class WorkerPoolTaskInfo extends node_async_hooks_1.AsyncResource {
11
+ callback;
12
+ constructor(callback) {
13
+ super('WorkerPoolTaskInfo');
14
+ this.callback = callback;
15
+ this.callback = callback;
16
+ }
17
+ done(err, result) {
18
+ this.runInAsyncScope(this.callback, null, err, result);
19
+ this.emitDestroy();
20
+ }
21
+ }
22
+ class WorkerPool extends node_events_1.EventEmitter {
23
+ processorFile;
24
+ numThreads;
25
+ workers = [];
26
+ freeWorkers = [];
27
+ workerTaskInfo = new Map();
28
+ tasks = [];
29
+ constructor(processorFile, numThreads = 0) {
30
+ super();
31
+ this.processorFile = processorFile;
32
+ this.numThreads = numThreads;
33
+ numThreads = +numThreads || (0, node_os_1.cpus)().length;
34
+ for (let i = 0; i < numThreads; i++)
35
+ this.addNewWorker(processorFile);
36
+ // 每当发出 kWorkerFreedEvent 时,调度队列中待处理的下一个任务(如果有)。
37
+ this.on(kWorkerFreedEvent, () => {
38
+ if (this.tasks.length > 0) {
39
+ const item = this.tasks.shift();
40
+ if (item)
41
+ this.runTask(item.task, item.callback);
42
+ }
43
+ });
44
+ }
45
+ addNewWorker(processorFile = this.processorFile) {
46
+ if (!(0, node_fs_1.existsSync)(processorFile)) {
47
+ throw Error(`Not Found: ${processorFile}`);
48
+ }
49
+ const worker = new node_worker_threads_1.Worker(processorFile);
50
+ worker.on('message', (result) => {
51
+ // 如果成功:调用传递给`runTask`的回调,删除与Worker关联的`TaskInfo`,并再次将其标记为空闲。
52
+ const r = this.workerTaskInfo.get(worker);
53
+ if (r) {
54
+ r.done(null, result);
55
+ this.workerTaskInfo.delete(worker);
56
+ this.freeWorkers.push(worker);
57
+ this.emit(kWorkerFreedEvent);
58
+ }
59
+ });
60
+ worker.on('error', err => {
61
+ // 如果发生未捕获的异常:调用传递给 `runTask` 并出现错误的回调。
62
+ const r = this.workerTaskInfo.get(worker);
63
+ if (r) {
64
+ r.done(err, null);
65
+ this.workerTaskInfo.delete(worker);
66
+ }
67
+ else
68
+ this.emit('error', err);
69
+ // 从列表中删除 Worker 并启动一个新的 Worker 来替换当前的 Worker
70
+ this.workers.splice(this.workers.indexOf(worker), 1);
71
+ this.addNewWorker();
72
+ });
73
+ this.workers.push(worker);
74
+ this.freeWorkers.push(worker);
75
+ this.emit(kWorkerFreedEvent);
76
+ }
77
+ runTask(task, callback) {
78
+ if (this.freeWorkers.length === 0) {
79
+ this.tasks.push({ task, callback });
80
+ return;
81
+ }
82
+ const worker = this.freeWorkers.pop();
83
+ if (worker) {
84
+ this.workerTaskInfo.set(worker, new WorkerPoolTaskInfo(callback));
85
+ worker.postMessage(task);
86
+ }
87
+ }
88
+ close() {
89
+ for (const worker of this.workers)
90
+ worker.terminate();
91
+ }
92
+ }
93
+ exports.WorkerPool = WorkerPool;
@@ -0,0 +1,2 @@
1
+ import { M3u8DLOptions } from './type';
2
+ export declare function m3u8BatchDownload(urls: string[], options: M3u8DLOptions): Promise<void>;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.m3u8BatchDownload = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const m3u8_download_1 = require("./lib/m3u8-download");
7
+ async function m3u8BatchDownload(urls, options) {
8
+ for (const url of urls) {
9
+ if ((0, node_fs_1.existsSync)(url)) {
10
+ const content = await node_fs_1.promises.readFile(url, 'utf8');
11
+ if (content.includes('.m3u8')) {
12
+ const list = content
13
+ .split('\n')
14
+ .filter(d => d.includes('.m3u8'))
15
+ .map((href, idx) => {
16
+ if (href.startsWith('http'))
17
+ href = `${idx}|${href}`;
18
+ return href;
19
+ });
20
+ if (!options.filename)
21
+ options.filename = (0, node_path_1.basename)(url).split('.')[0];
22
+ await m3u8BatchDownload(list, options);
23
+ continue;
24
+ }
25
+ }
26
+ await (0, m3u8_download_1.m3u8Download)(url, options);
27
+ }
28
+ }
29
+ exports.m3u8BatchDownload = m3u8BatchDownload;
package/cjs/type.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ /// <reference path="../global.d.ts" />
2
+ /// <reference types="node" />
3
+ /// <reference types="node" />
4
+ import type { IncomingHttpHeaders } from 'node:http';
5
+ export interface TsItemInfo {
6
+ /** ts 文件次序 */
7
+ index: number;
8
+ duration: number;
9
+ timeline: number;
10
+ /** ts 文件下载 url 地址 */
11
+ uri: string;
12
+ /** ts 文件下载保存路径 */
13
+ tsOut: string;
14
+ /** 是否下载成功 */
15
+ success?: boolean;
16
+ }
17
+ export interface M3u8Crypto {
18
+ /** AES 加密 IV */
19
+ iv: NodeJS.ArrayBufferView | string;
20
+ /** 获取到的密钥值(hex) */
21
+ key: string | NodeJS.ArrayBufferView;
22
+ /** 加密方法 */
23
+ method: string;
24
+ /** 密钥获取接口 */
25
+ uri: string;
26
+ }
27
+ export interface M3u8DLOptions {
28
+ debug?: boolean;
29
+ /** 是否显示内置的进度信息。默认为 true */
30
+ showProgress?: boolean;
31
+ /** 每当 ts 文件下载完成时回调,可用于自定义进度控制 */
32
+ onProgress?: (finished: number, total: number, currentInfo: TsItemInfo) => void;
33
+ /** 并发下载线程数。取决于服务器限制,过多可能会容易下载失败。一般建议不超过 4 个。默认为 cpu 数量 */
34
+ threadNum?: number;
35
+ /** 要保存的文件名(路径) */
36
+ filename?: string;
37
+ /** 下载文件保存的路径。默认为当前目录 */
38
+ saveDir?: string;
39
+ /** 临时文件保存目录。默认为 cache/<md5(url)> */
40
+ cacheDir?: string;
41
+ /** 下载成功后是否删除临时文件。默认为 true。保存临时文件可以在重复下载时识别缓存 */
42
+ delCache?: boolean;
43
+ /** 文件已存在时是否仍强制下载和生成。默认为 false,文件已存在则跳过 */
44
+ force?: boolean;
45
+ /** 下载 m3u8、ts 等文件时自定义请求 headers */
46
+ headers?: IncomingHttpHeaders;
47
+ /** 下载时是否启动本地资源播放(边下边看) */
48
+ play?: boolean;
49
+ }
50
+ export interface WorkerTaskInfo {
51
+ info: TsItemInfo;
52
+ crypto: M3u8Crypto;
53
+ options: M3u8DLOptions;
54
+ }
package/global.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module 'm3u8-parser';
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@lzwme/m3u8-dl",
3
+ "version": "0.0.1",
4
+ "description": "Batch download of m3u8 files and convert to mp4",
5
+ "main": "cjs/index.js",
6
+ "types": "cjs/index.d.ts",
7
+ "license": "MIT",
8
+ "repository": "https://github.com/lzwme/m3u8-dl.git",
9
+ "author": {
10
+ "name": "renxia",
11
+ "email": "lzwy0820@qq.com",
12
+ "url": "https://lzw.me"
13
+ },
14
+ "scripts": {
15
+ "prepare": "husky install",
16
+ "dev": "npm run watch",
17
+ "watch": "npm run build -- -- -w",
18
+ "lint": "flh --eslint --tscheck --prettier",
19
+ "build": "npm run clean && npm run build:cjs",
20
+ "build:cjs": "tsc -p tsconfig.cjs.json",
21
+ "doc": "typedoc src/ --exclude **/*.spec.ts --out docs --tsconfig tsconfig.module.json",
22
+ "version": "standard-version",
23
+ "dist": "npm run build",
24
+ "release": "npm run dist && npm run version",
25
+ "clean": "flh rm -f ./cjs ./esm ./docs",
26
+ "test": "npm run lint"
27
+ },
28
+ "bin": {
29
+ "m3u8dl": "bin/m3u8dl.js"
30
+ },
31
+ "keywords": [
32
+ "m3u8-download",
33
+ "m3u8",
34
+ "mp4",
35
+ "download",
36
+ "ffmpeg"
37
+ ],
38
+ "packageManager": "pnpm@7.6.0",
39
+ "engines": {
40
+ "node": ">=14.18"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public",
44
+ "registry": "https://registry.npmjs.com"
45
+ },
46
+ "devDependencies": {
47
+ "@lzwme/fed-lint-helper": "^2.2.0",
48
+ "@types/node": "^18.11.18",
49
+ "@typescript-eslint/eslint-plugin": "^5.49.0",
50
+ "@typescript-eslint/parser": "^5.49.0",
51
+ "eslint": "^8.32.0",
52
+ "eslint-config-prettier": "^8.6.0",
53
+ "eslint-plugin-prettier": "^4.2.1",
54
+ "husky": "^8.0.3",
55
+ "prettier": "^2.8.3",
56
+ "standard-version": "^9.5.0",
57
+ "typescript": "^4.9.4"
58
+ },
59
+ "dependencies": {
60
+ "@lzwme/fe-utils": "^1.3.3",
61
+ "commander": "^10.0.0",
62
+ "console-log-colors": "^0.3.3",
63
+ "m3u8-parser": "^6.0.0"
64
+ },
65
+ "files": [
66
+ "cjs",
67
+ "!cjs/type.js",
68
+ "!cjs/cli.d.ts",
69
+ "global.d.ts",
70
+ "bin"
71
+ ]
72
+ }