@lzwme/m3u8-dl 0.0.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.MD +29 -0
- package/bin/m3u8dl.js +0 -0
- package/cjs/cli.js +48 -2
- package/cjs/lib/local-play.d.ts +3 -3
- package/cjs/lib/local-play.js +25 -11
- package/cjs/lib/m3u8-convert.js +10 -2
- package/cjs/lib/m3u8-download.d.ts +27 -8
- package/cjs/lib/m3u8-download.js +193 -53
- package/cjs/lib/parseM3u8.d.ts +7 -11
- package/cjs/lib/parseM3u8.js +20 -13
- package/cjs/lib/search-api/ApiManage.d.ts +30 -0
- package/cjs/lib/search-api/ApiManage.js +90 -0
- package/cjs/lib/search-api/CommSearchApi.d.ts +44 -0
- package/cjs/lib/search-api/CommSearchApi.js +80 -0
- package/cjs/lib/storage.d.ts +6 -4
- package/cjs/lib/ts-download.js +15 -9
- package/cjs/lib/utils.d.ts +2 -0
- package/cjs/lib/utils.js +20 -0
- package/cjs/lib/video-search.d.ts +2 -54
- package/cjs/lib/video-search.js +15 -135
- package/cjs/lib/worker_pool.d.ts +4 -1
- package/cjs/lib/worker_pool.js +8 -2
- package/cjs/m3u8-batch-download.js +19 -9
- package/cjs/server/download-server.d.ts +41 -0
- package/cjs/server/download-server.js +466 -0
- package/cjs/server/index.d.ts +2 -0
- package/cjs/server/index.js +5 -0
- package/cjs/types/m3u8.d.ts +84 -2
- package/cjs/types/m3u8.js +7 -0
- package/cjs/types/video-search.d.ts +119 -109
- package/client/index.html +950 -0
- package/package.json +21 -16
package/README.MD
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
- 支持常见的 AES 加密视频流解密。
|
|
26
26
|
- 自动转换为 mp4。**需全局安装 [ffmpeg](https://ffmpeg.org/download.html)**
|
|
27
27
|
- `[NEW!]`支持指定采集站标准 API,以命令行交互的方式搜索和下载。
|
|
28
|
+
- `[NEW!]` 新增下载中心,支持启动为 webui 服务方式进行下载管理
|
|
28
29
|
|
|
29
30
|
## 安装(Install)
|
|
30
31
|
|
|
@@ -91,6 +92,34 @@ m3u8dl s -u https://jyzyapi.com/provide/vod/
|
|
|
91
92
|
|
|
92
93
|
**声明:** 以上仅作示例,请自行搜索查找可用的采集站 API。本工具仅用作技术研究学习,不提供任何具体资源类信息。
|
|
93
94
|
|
|
95
|
+
### 命令行方式启动 webui
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# 安装 server 需要的依赖
|
|
99
|
+
npm i -g express ws
|
|
100
|
+
# 启动 server
|
|
101
|
+
m3u8dl server -p 6600
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
然后浏览器访问: http://localhost:6600
|
|
105
|
+
|
|
106
|
+

|
|
107
|
+
|
|
108
|
+
## 基于 Docker 部署
|
|
109
|
+
|
|
110
|
+
基于 docker 命令:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
docker run
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
基于 `docker-compose.yml`:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
m3u8dl:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
|
|
94
123
|
### API 调用
|
|
95
124
|
|
|
96
125
|
```ts
|
package/bin/m3u8dl.js
CHANGED
|
File without changes
|
package/cjs/cli.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
const node_path_1 = require("node:path");
|
|
4
37
|
const commander_1 = require("commander");
|
|
@@ -9,7 +42,7 @@ const m3u8_batch_download_1 = require("./m3u8-batch-download");
|
|
|
9
42
|
const video_search_js_1 = require("./lib/video-search.js");
|
|
10
43
|
const pkg = (0, fe_utils_1.readJsonFileSync)((0, node_path_1.resolve)(__dirname, '../package.json'));
|
|
11
44
|
process.on('unhandledRejection', (r, p) => {
|
|
12
|
-
|
|
45
|
+
utils_js_1.logger.info('[退出][unhandledRejection]', r, p);
|
|
13
46
|
process.exit();
|
|
14
47
|
});
|
|
15
48
|
process.on('SIGINT', signal => {
|
|
@@ -30,22 +63,35 @@ commander_1.program
|
|
|
30
63
|
.option('-C, --cache-dir <dirpath>', `临时文件保存目录。默认为 cache`)
|
|
31
64
|
.option('-S, --save-dir <dirpath>', `下载文件保存的路径。默认为当前目录`)
|
|
32
65
|
.option('--no-del-cache', `下载成功后是否删除临时文件。默认为 true。保存临时文件可以在重复下载时识别缓存`)
|
|
66
|
+
.option('--no-convert', '下载成功后,是否不合并转换为 mp4 文件。默认为 true。')
|
|
33
67
|
.action(async (urls) => {
|
|
34
68
|
const options = getOptions();
|
|
35
69
|
utils_js_1.logger.debug(urls, options);
|
|
36
70
|
if (options.progress != null)
|
|
37
71
|
options.showProgress = options.progress;
|
|
72
|
+
delete options.progress;
|
|
38
73
|
if (urls.length > 0) {
|
|
39
74
|
await (0, m3u8_batch_download_1.m3u8BatchDownload)(urls, options);
|
|
40
75
|
}
|
|
41
76
|
else
|
|
42
77
|
commander_1.program.help();
|
|
43
78
|
});
|
|
79
|
+
commander_1.program
|
|
80
|
+
.command('server')
|
|
81
|
+
.description('启动下载中心web服务')
|
|
82
|
+
.option('-P, --port <port>', '指定web服务端口。默认为6600')
|
|
83
|
+
.action((options) => {
|
|
84
|
+
const opts = Object.assign(getOptions(), options);
|
|
85
|
+
Promise.resolve().then(() => __importStar(require('./server/download-server.js'))).then(m => {
|
|
86
|
+
new m.DLServer({ port: opts.port || 6600, debug: opts.debug });
|
|
87
|
+
});
|
|
88
|
+
});
|
|
44
89
|
commander_1.program
|
|
45
90
|
.command('search [keyword]')
|
|
46
91
|
.alias('s')
|
|
47
92
|
.option('-u,--url <api...>', '影视搜索的接口地址(m3u8采集站标准接口)')
|
|
48
|
-
.option('-
|
|
93
|
+
.option('-d, --apidir <dirpath>', '指定自定义视频搜索 api 所在的目录或具体路径')
|
|
94
|
+
// .option('-R,--remote-config-url <url>', '自定义远程配置加载地址。默认从主仓库配置读取')
|
|
49
95
|
.description('m3u8视频在线搜索与下载')
|
|
50
96
|
.action(async (keyword, options) => {
|
|
51
97
|
await (0, video_search_js_1.VideoSerachAndDL)(keyword, options, getOptions());
|
package/cjs/lib/local-play.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { TsItemInfo } from '../types/m3u8.js';
|
|
2
2
|
/**
|
|
3
3
|
* 边下边看
|
|
4
4
|
*/
|
|
5
|
-
export declare function localPlay(m3u8Info: TsItemInfo[]
|
|
5
|
+
export declare function localPlay(m3u8Info: TsItemInfo[]): Promise<{
|
|
6
6
|
port: number;
|
|
7
7
|
origin: string;
|
|
8
8
|
server: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
|
|
9
9
|
}>;
|
|
10
|
-
export declare function toLocalM3u8(m3u8Info: TsItemInfo[],
|
|
10
|
+
export declare function toLocalM3u8(m3u8Info: TsItemInfo[], m3u8FilePath?: string, host?: string): string;
|
package/cjs/lib/local-play.js
CHANGED
|
@@ -7,24 +7,30 @@ const console_log_colors_1 = require("console-log-colors");
|
|
|
7
7
|
const node_fs_1 = require("node:fs");
|
|
8
8
|
const node_http_1 = require("node:http");
|
|
9
9
|
const node_path_1 = require("node:path");
|
|
10
|
-
const
|
|
10
|
+
const utils_js_1 = require("./utils.js");
|
|
11
11
|
/**
|
|
12
12
|
* 边下边看
|
|
13
13
|
*/
|
|
14
|
-
async function localPlay(m3u8Info
|
|
14
|
+
async function localPlay(m3u8Info) {
|
|
15
15
|
if (!m3u8Info?.length)
|
|
16
16
|
return null;
|
|
17
17
|
const cacheDir = (0, node_path_1.dirname)(m3u8Info[0].tsOut);
|
|
18
18
|
const cacheDirname = (0, node_path_1.basename)(cacheDir);
|
|
19
|
+
const cacheFilepath = toLocalM3u8(m3u8Info);
|
|
20
|
+
const filename = (0, node_path_1.basename)(cacheFilepath);
|
|
19
21
|
const info = await createLocalServer((0, node_path_1.dirname)(cacheDir));
|
|
20
|
-
const filename = (0, node_path_1.basename)(options.filename).slice(0, options.filename.lastIndexOf('.')) + `.m3u8`;
|
|
21
|
-
await toLocalM3u8(m3u8Info, (0, node_path_1.resolve)(cacheDir, filename), `/${cacheDirname}`);
|
|
22
22
|
const playUrl = `https://lzw.me/x/m3u8-player?url=${encodeURIComponent(`${info.origin}/${cacheDirname}/${filename}`)}`;
|
|
23
23
|
const cmd = `${process.platform === 'win32' ? 'start' : 'open'} ${playUrl}`;
|
|
24
24
|
(0, fe_utils_1.execSync)(cmd);
|
|
25
25
|
return info;
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
function toLocalM3u8(m3u8Info, m3u8FilePath = '', host = '') {
|
|
28
|
+
const cacheDir = (0, node_path_1.dirname)(m3u8Info[0].tsOut);
|
|
29
|
+
const cacheDirname = (0, node_path_1.basename)(cacheDir);
|
|
30
|
+
if (!m3u8FilePath)
|
|
31
|
+
m3u8FilePath = (0, node_path_1.resolve)(cacheDir, `${cacheDirname}.m3u8`);
|
|
32
|
+
if ((0, node_fs_1.existsSync)(m3u8FilePath))
|
|
33
|
+
return m3u8FilePath;
|
|
28
34
|
const m3u8ContentList = [
|
|
29
35
|
`#EXTM3U`,
|
|
30
36
|
`#EXT-X-VERSION:3`,
|
|
@@ -33,13 +39,21 @@ async function toLocalM3u8(m3u8Info, filepath, host = '') {
|
|
|
33
39
|
`#EXT-X-MEDIA-SEQUENCE:0`,
|
|
34
40
|
// `#EXT-X-KEY:METHOD=AES-128,URI="/api/aes/enc.key"`,
|
|
35
41
|
];
|
|
42
|
+
if (host && !host.endsWith('/'))
|
|
43
|
+
host += '/';
|
|
36
44
|
m3u8Info.forEach(d => {
|
|
37
45
|
if (d.tsOut)
|
|
38
|
-
m3u8ContentList.push(`#EXTINF:${Number(d.duration).toFixed(6)},`, `${host}
|
|
46
|
+
m3u8ContentList.push(`#EXTINF:${Number(d.duration).toFixed(6)},`, `${host}${(0, node_path_1.basename)(d.tsOut)}`);
|
|
39
47
|
});
|
|
40
48
|
m3u8ContentList.push(`#EXT-X-ENDLIST`);
|
|
41
49
|
const m3u8Content = m3u8ContentList.join('\n');
|
|
42
|
-
|
|
50
|
+
const ext = (0, node_path_1.extname)(m3u8FilePath);
|
|
51
|
+
if (ext !== '.m3u8')
|
|
52
|
+
m3u8FilePath = m3u8FilePath.replace(ext, '') + '.m3u8';
|
|
53
|
+
m3u8FilePath = (0, node_path_1.resolve)(cacheDir, m3u8FilePath);
|
|
54
|
+
(0, fe_utils_1.mkdirp)(cacheDir);
|
|
55
|
+
(0, node_fs_1.writeFileSync)(m3u8FilePath, m3u8Content, 'utf8');
|
|
56
|
+
return m3u8FilePath;
|
|
43
57
|
}
|
|
44
58
|
async function createLocalServer(baseDir) {
|
|
45
59
|
baseDir = (0, node_path_1.resolve)(baseDir);
|
|
@@ -47,7 +61,7 @@ async function createLocalServer(baseDir) {
|
|
|
47
61
|
const origin = `http://localhost:${port}`;
|
|
48
62
|
const server = (0, node_http_1.createServer)((req, res) => {
|
|
49
63
|
const filename = (0, node_path_1.join)(baseDir, decodeURIComponent(req.url));
|
|
50
|
-
|
|
64
|
+
utils_js_1.logger.debug('[req]', req.url, filename);
|
|
51
65
|
if ((0, node_fs_1.existsSync)(filename)) {
|
|
52
66
|
const stats = (0, node_fs_1.statSync)(filename);
|
|
53
67
|
const ext = (0, node_path_1.extname)(filename);
|
|
@@ -66,8 +80,8 @@ async function createLocalServer(baseDir) {
|
|
|
66
80
|
return;
|
|
67
81
|
}
|
|
68
82
|
else if (stats.isDirectory()) {
|
|
69
|
-
const html = (0, node_fs_1.readdirSync)(filename).map(
|
|
70
|
-
const rpath = (0, node_path_1.resolve)(filename,
|
|
83
|
+
const html = (0, node_fs_1.readdirSync)(filename).map(fname => {
|
|
84
|
+
const rpath = (0, node_path_1.resolve)(filename, fname).replace(baseDir, '');
|
|
71
85
|
return `<li><a href="${rpath}">${rpath}</a></li>`;
|
|
72
86
|
});
|
|
73
87
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
@@ -79,7 +93,7 @@ async function createLocalServer(baseDir) {
|
|
|
79
93
|
res.end('Not found');
|
|
80
94
|
}).listen(port, () => {
|
|
81
95
|
console.log();
|
|
82
|
-
|
|
96
|
+
utils_js_1.logger.info('Created Local Server:', console_log_colors_1.color.greenBright(origin));
|
|
83
97
|
});
|
|
84
98
|
return { port, origin, server };
|
|
85
99
|
}
|
package/cjs/lib/m3u8-convert.js
CHANGED
|
@@ -14,18 +14,20 @@ async function m3u8Convert(options, data) {
|
|
|
14
14
|
if (!options.force && (0, node_fs_1.existsSync)(filepath))
|
|
15
15
|
return filepath;
|
|
16
16
|
utils_1.logger.info(`Starting ${ffmpegSupport ? 'convert to mp4' : 'merge into ts'} file:`, (0, console_log_colors_1.greenBright)(filepath));
|
|
17
|
+
(0, fe_utils_1.mkdirp)((0, node_path_1.dirname)(filepath));
|
|
17
18
|
if (ffmpegSupport) {
|
|
18
19
|
const inputFilePath = (0, node_path_1.resolve)(options.cacheDir, 'input.txt');
|
|
19
20
|
let filesAllArr = data.map(d => (0, node_path_1.resolve)(d.tsOut)).filter(d => (0, node_fs_1.existsSync)(d));
|
|
20
21
|
if (process.platform === 'win32')
|
|
21
22
|
filesAllArr = filesAllArr.map(d => d.replaceAll('\\', '/'));
|
|
22
|
-
|
|
23
|
+
(0, node_fs_1.writeFileSync)(inputFilePath, 'ffconcat version 1.0\nfile ' + filesAllArr.join('\nfile '));
|
|
23
24
|
let headersString = '';
|
|
24
25
|
if (options.headers) {
|
|
25
26
|
for (const [key, value] of Object.entries(options.headers)) {
|
|
26
27
|
headersString += `-headers "${key}: ${String(value)}" `;
|
|
27
28
|
}
|
|
28
29
|
}
|
|
30
|
+
// ffmpeg -i nz.ts -c copy -map 0:v -map 0:a -bsf:a aac_adtstoasc nz.mp4
|
|
29
31
|
const cmd = `ffmpeg -y -f concat -safe 0 -i ${inputFilePath} -acodec copy -vcodec copy -bsf:a aac_adtstoasc ${headersString} "${filepath}"`;
|
|
30
32
|
utils_1.logger.debug('[convert to mp4]cmd:', (0, console_log_colors_1.cyan)(cmd));
|
|
31
33
|
const r = (0, fe_utils_1.execSync)(cmd);
|
|
@@ -35,7 +37,13 @@ async function m3u8Convert(options, data) {
|
|
|
35
37
|
}
|
|
36
38
|
if (!ffmpegSupport) {
|
|
37
39
|
filepath = filepath.replace(/\.mp4$/, '.ts');
|
|
38
|
-
|
|
40
|
+
const filteWriteStream = (0, node_fs_1.createWriteStream)(filepath);
|
|
41
|
+
for (const d of data) {
|
|
42
|
+
const err = await new Promise(rs => filteWriteStream.write((0, node_fs_1.readFileSync)(d.tsOut), e => rs(e)));
|
|
43
|
+
if (err)
|
|
44
|
+
utils_1.logger.error(`Write file failed: ${d.tsOut}`, err);
|
|
45
|
+
}
|
|
46
|
+
filteWriteStream.end();
|
|
39
47
|
}
|
|
40
48
|
if (!(0, node_fs_1.existsSync)(filepath))
|
|
41
49
|
return '';
|
|
@@ -1,13 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { parseM3U8 } from './parseM3u8.js';
|
|
2
|
+
import type { M3u8DLOptions, M3u8WorkerPool } from '../types/m3u8.js';
|
|
3
|
+
export declare class DownloadQueue {
|
|
4
|
+
private queue;
|
|
5
|
+
private activeDownloads;
|
|
6
|
+
private _maxConcurrent;
|
|
7
|
+
constructor(maxConcurrent?: number);
|
|
8
|
+
get maxConcurrent(): number;
|
|
9
|
+
set maxConcurrent(value: number);
|
|
10
|
+
add(url: string, options: M3u8DLOptions, priority?: number): void;
|
|
11
|
+
private processQueue;
|
|
12
|
+
pause(url: string): void;
|
|
13
|
+
resume(url: string, options: M3u8DLOptions, priority?: number): void;
|
|
14
|
+
clear(): void;
|
|
15
|
+
getStatus(): {
|
|
16
|
+
queueLength: number;
|
|
17
|
+
activeDownloads: string[];
|
|
18
|
+
maxConcurrent: number;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare const downloadQueue: DownloadQueue;
|
|
22
|
+
export declare const workPollPublic: M3u8WorkerPool;
|
|
23
|
+
export declare function preDownLoad(url: string, options: M3u8DLOptions, wp?: M3u8WorkerPool): Promise<void>;
|
|
9
24
|
export declare function m3u8Download(url: string, options?: M3u8DLOptions): Promise<{
|
|
25
|
+
filepath?: string;
|
|
26
|
+
error?: Error;
|
|
27
|
+
} | {
|
|
10
28
|
options: M3u8DLOptions;
|
|
11
29
|
m3u8Info: Awaited<ReturnType<typeof parseM3U8>> | null;
|
|
12
30
|
filepath: string;
|
|
13
31
|
}>;
|
|
32
|
+
export declare function m3u8DLStop(url: string, wp?: M3u8WorkerPool): number;
|