@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 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
+ ![](examples/img/m3u8dl-server-webui.jpg)
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
- console.log('[退出][unhandledRejection]', r, p);
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('-R,--remote-config-url <url>', '自定义远程配置加载地址。默认从主仓库配置读取')
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());
@@ -1,10 +1,10 @@
1
- import { M3u8DLOptions, TsItemInfo } from '../types/m3u8';
1
+ import type { TsItemInfo } from '../types/m3u8.js';
2
2
  /**
3
3
  * 边下边看
4
4
  */
5
- export declare function localPlay(m3u8Info: TsItemInfo[], options: M3u8DLOptions): Promise<{
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[], filepath: string, host?: string): Promise<void>;
10
+ export declare function toLocalM3u8(m3u8Info: TsItemInfo[], m3u8FilePath?: string, host?: string): string;
@@ -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 utils_1 = require("./utils");
10
+ const utils_js_1 = require("./utils.js");
11
11
  /**
12
12
  * 边下边看
13
13
  */
14
- async function localPlay(m3u8Info, options) {
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
- async function toLocalM3u8(m3u8Info, filepath, host = '') {
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}/${(0, node_path_1.basename)(d.tsOut)}`);
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
- await node_fs_1.promises.writeFile(filepath, m3u8Content, 'utf8');
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
- utils_1.logger.debug('[req]', req.url, filename);
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(filepath => {
70
- const rpath = (0, node_path_1.resolve)(filename, filepath).replace(baseDir, '');
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
- utils_1.logger.info('Created Local Server:', console_log_colors_1.color.greenBright(origin));
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
  }
@@ -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
- await node_fs_1.promises.writeFile(inputFilePath, 'ffconcat version 1.0\nfile ' + filesAllArr.join('\nfile '));
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
- await node_fs_1.promises.writeFile(filepath, Buffer.concat(data.map(d => (0, node_fs_1.readFileSync)(d.tsOut))));
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 { WorkerPool } from './worker_pool';
2
- import { parseM3U8 } from './parseM3u8';
3
- import type { M3u8DLOptions, TsItemInfo, WorkerTaskInfo } from '../types/m3u8';
4
- export declare const workPoll: WorkerPool<WorkerTaskInfo, {
5
- success: boolean;
6
- info: TsItemInfo;
7
- }>;
8
- export declare function preDownLoad(url: string, options: M3u8DLOptions): Promise<void>;
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;