@lzwme/m3u8-dl 1.6.0 → 1.8.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
@@ -12,7 +12,7 @@
12
12
  [![GitHub forks][forks-badge]][forks-url]
13
13
  [![GitHub stars][stars-badge]][stars-url]
14
14
 
15
- > **Language**: [English](README.md) | [中文简体](README.zh-CN.MD)
15
+ > **Language**: [English](README.md) | [中文简体](README.zh-CN.md)
16
16
 
17
17
  A free, open-source, and powerful m3u8 video batch downloader with multi-threaded downloading, play-while-downloading, WebUI management, video parsing, and more. Supports multiple usage methods including CLI command line, browser, PC client, Docker deployment, and Node.js API calls.
18
18
 
@@ -59,12 +59,12 @@ A free, open-source, and powerful m3u8 video batch downloader with multi-threade
59
59
  - **Auto Capture**: Automatically intercepts and captures m3u8 and mp4 video links in web pages
60
60
  - **Real-time Monitoring**: Monitors network requests (XMLHttpRequest, fetch, Performance API), automatically discovers video links
61
61
  - **Smart Recognition**: Automatically recognizes video types (M3U8/MP4) and extracts video names
62
- - **One-click Jump**: Supports one-click jump to m3u8-dl WebUI for downloading
62
+ - **One-click Jump**: Supports one-click jump to M3U8-DL WebUI for downloading
63
63
  - **Flexible Configuration**: Supports configuring exclusion URL rules to avoid capturing on specific pages
64
64
  - **Draggable Panel**: Supports dragging to move panel position, automatically saves position
65
65
  - **Link Management**: Supports copying links, clearing list, and other operations
66
66
 
67
- > This is a Tampermonkey/Greasemonkey userscript that can automatically capture video links in browsers, working with m3u8-dl WebUI to provide a seamless download experience.
67
+ > This is a Violentmonkey/Tampermonkey/Greasemonkey userscript that can automatically capture video links in browsers, working with M3U8-DL WebUI to provide a seamless download experience.
68
68
 
69
69
  ### 📺 Video Search Features
70
70
 
@@ -418,11 +418,11 @@ Download the built application:
418
418
 
419
419
  1. Install browser extension (choose one):
420
420
  - [Violentmonkey](https://violentmonkey.github.io/) (【Recommended】Open-source alternative)
421
- - [Tampermonkey](https://www.tampermonkey.net/) (【Official】Supports Chrome, Firefox, Edge, Safari, etc.)
421
+ - [Tampermonkey](https://www.tampermonkey.net/) (Supports Chrome, Firefox, Edge, Safari, etc.)
422
422
  - [Greasemonkey](https://www.greasespot.net/) (Firefox only)
423
423
 
424
424
  2. Install script:
425
- - Open Tampermonkey management panel
425
+ - Open userscript manager (Violentmonkey/Tampermonkey) management panel
426
426
  - Click "Add new script"
427
427
  - Copy content from `client/m3u8-capture.user.js` file
428
428
  - Paste into editor and save
@@ -431,14 +431,14 @@ Download the built application:
431
431
  3. Configure WebUI address:
432
432
  - Visit any web page, click the 🎬 icon in the top right corner to open the capture panel
433
433
  - Click the settings button ⚙️
434
- - Enter your m3u8-dl WebUI address (e.g., `http://localhost:6600`)
434
+ - Enter your M3U8-DL WebUI address (e.g., `http://localhost:6600`)
435
435
  - Save settings
436
436
 
437
437
  **Feature Description:**
438
438
 
439
439
  - **Auto Capture**: The script automatically monitors network requests in web pages, when m3u8 or mp4 video links are detected, automatically adds them to the list
440
440
  - **Video Name Extraction**: Prioritizes extracting video names from page `h1`, `h2`, or `document.title`
441
- - **Jump to Download**: Click the "Jump to Download" button to automatically jump to m3u8-dl WebUI and fill in video link and name (format: `url|name`)
441
+ - **Jump to Download**: Click the "Jump to Download" button to automatically jump to M3U8-DL WebUI and fill in video link and name (format: `url|name`)
442
442
  - **Exclusion Rules**: In settings, you can configure exclusion URL rule list, matching URLs will not show panel and will not capture video links
443
443
  - Supports plain string matching (contains match)
444
444
  - Supports regular expressions (starts and ends with `/`, e.g., `/example\.com/`)
@@ -448,7 +448,7 @@ Download the built application:
448
448
  1. Visit video playback page
449
449
  2. Script automatically captures video links, displayed in the panel in the bottom right corner
450
450
  3. Click "Jump to Download" button
451
- 4. Automatically jumps to m3u8-dl WebUI, video link and name are automatically filled
451
+ 4. Automatically jumps to M3U8-DL WebUI, video link and name are automatically filled
452
452
  5. Click "Start Download" in WebUI
453
453
 
454
454
  **Exclusion Rule Configuration Example:**
package/README.zh-CN.md CHANGED
@@ -12,7 +12,7 @@
12
12
  [![GitHub forks][forks-badge]][forks-url]
13
13
  [![GitHub stars][stars-badge]][stars-url]
14
14
 
15
- > **语言**: [English](README.md) | [中文简体](README.zh-CN.MD)
15
+ > **语言**: [English](README.md) | [中文简体](README.zh-CN.md)
16
16
 
17
17
  一个免费开源功能强大的 m3u8 视频批量下载工具,支持多线程下载、边下边播、WebUI 管理、视频解析等多种功能。支持 CLI命令行、浏览器、PC客户端、Docker 部署以及 Node.js API 调用等多种使用方式。
18
18
 
@@ -59,12 +59,12 @@
59
59
  - **自动抓取**:自动拦截和抓取网页中的 m3u8 和 mp4 视频链接
60
60
  - **实时监控**:监控网络请求(XMLHttpRequest、fetch、Performance API),自动发现视频链接
61
61
  - **智能识别**:自动识别视频类型(M3U8/MP4),并提取视频名称
62
- - **一键跳转**:支持一键跳转到 m3u8-dl WebUI 进行下载
62
+ - **一键跳转**:支持一键跳转到 M3U8-DL WebUI 进行下载
63
63
  - **灵活配置**:支持配置排除网址规则,避免在特定页面抓取
64
64
  - **拖拽面板**:支持拖拽移动面板位置,自动保存位置
65
65
  - **链接管理**:支持复制链接、清空列表等操作
66
66
 
67
- > 这是一个 Tampermonkey/Greasemonkey 用户脚本,可在浏览器中自动抓取视频链接,配合 m3u8-dl WebUI 使用,实现无缝下载体验。
67
+ > 这是一个 Violentmonkey/Tampermonkey/Greasemonkey 用户脚本,可在浏览器中自动抓取视频链接,配合 M3U8-DL WebUI 使用,实现无缝下载体验。
68
68
 
69
69
  ### 📺 视频搜索功能
70
70
 
@@ -418,11 +418,11 @@ docker-compose up -d
418
418
 
419
419
  1. 安装浏览器扩展(任选其一):
420
420
  - [Violentmonkey](https://violentmonkey.github.io/)(【推荐】开源替代方案)
421
- - [Tampermonkey](https://www.tampermonkey.net/)(【官方】支持 Chrome、Firefox、Edge、Safari 等)
421
+ - [Tampermonkey](https://www.tampermonkey.net/)(支持 Chrome、Firefox、Edge、Safari 等)
422
422
  - [Greasemonkey](https://www.greasespot.net/)(仅支持 Firefox)
423
423
 
424
424
  2. 安装脚本:
425
- - 打开 Tampermonkey 管理面板
425
+ - 打开用户脚本管理器(Violentmonkey/Tampermonkey)管理面板
426
426
  - 点击"添加新脚本"
427
427
  - 复制 `client/m3u8-capture.user.js` 文件内容
428
428
  - 粘贴到编辑器中并保存
@@ -431,14 +431,14 @@ docker-compose up -d
431
431
  3. 配置 WebUI 地址:
432
432
  - 访问任意网页,点击页面右上角的 🎬 图标打开抓取面板
433
433
  - 点击设置按钮 ⚙️
434
- - 输入您的 m3u8-dl WebUI 地址(如 `http://localhost:6600`)
434
+ - 输入您的 M3U8-DL WebUI 地址(如 `http://localhost:6600`)
435
435
  - 保存设置
436
436
 
437
437
  **功能说明:**
438
438
 
439
439
  - **自动抓取**:脚本会自动监控网页中的网络请求,当检测到 m3u8 或 mp4 视频链接时,自动添加到列表中
440
440
  - **视频名称提取**:优先从页面的 `h1`、`h2` 或 `document.title` 提取视频名称
441
- - **跳转下载**:点击"跳转下载"按钮,会自动跳转到 m3u8-dl WebUI 并填充视频链接和名称(格式:`url|name`)
441
+ - **跳转下载**:点击"跳转下载"按钮,会自动跳转到 M3U8-DL WebUI 并填充视频链接和名称(格式:`url|name`)
442
442
  - **排除规则**:在设置中可以配置排除网址规则列表,匹配的网址将不展示面板且不抓取视频链接
443
443
  - 支持普通字符串匹配(包含匹配)
444
444
  - 支持正则表达式(以 `/` 开头和结尾,如 `/example\.com/`)
@@ -448,7 +448,7 @@ docker-compose up -d
448
448
  1. 访问视频播放页面
449
449
  2. 脚本自动抓取视频链接,显示在右下角的面板中
450
450
  3. 点击"跳转下载"按钮
451
- 4. 自动跳转到 m3u8-dl 的 WebUI,视频链接和名称已自动填充
451
+ 4. 自动跳转到 M3U8-DL 的 WebUI,视频链接和名称已自动填充
452
452
  5. 在 WebUI 中点击"开始下载"即可
453
453
 
454
454
  **排除规则配置示例:**
@@ -43,6 +43,14 @@ declare const _default: {
43
43
  accessDenied: string;
44
44
  invalidUrl: string;
45
45
  notFound: string;
46
+ invalidParams: string;
47
+ taskNotFound: string;
48
+ onlyRenameCompleted: string;
49
+ invalidFilename: string;
50
+ fileNotFound: string;
51
+ fileExists: string;
52
+ renameFailed: string;
53
+ duplicateDownload: string;
46
54
  };
47
55
  success: {
48
56
  configUpdated: string;
@@ -52,6 +60,7 @@ declare const _default: {
52
60
  resumed: string;
53
61
  noResumableTasks: string;
54
62
  deleted: string;
63
+ renamed: string;
55
64
  };
56
65
  };
57
66
  download: {
@@ -45,6 +45,14 @@ exports.default = {
45
45
  accessDenied: 'Access denied',
46
46
  invalidUrl: 'Invalid url parameter',
47
47
  notFound: 'Not Found',
48
+ invalidParams: 'Invalid parameters',
49
+ taskNotFound: 'Task not found',
50
+ onlyRenameCompleted: 'Only completed tasks with normal status can be renamed',
51
+ invalidFilename: 'Filename contains invalid characters',
52
+ fileNotFound: 'File not found',
53
+ fileExists: 'File already exists',
54
+ renameFailed: 'Rename failed: {error}',
55
+ duplicateDownload: '{count} URL(s) already exist, duplicate download',
48
56
  },
49
57
  success: {
50
58
  configUpdated: 'Config updated successfully',
@@ -54,6 +62,7 @@ exports.default = {
54
62
  resumed: 'Resumed {count} download task(s)',
55
63
  noResumableTasks: 'No resumable download tasks found',
56
64
  deleted: 'Deleted {count} download task(s)',
65
+ renamed: 'Renamed successfully',
57
66
  },
58
67
  },
59
68
  download: {
@@ -43,6 +43,14 @@ declare const _default: {
43
43
  accessDenied: string;
44
44
  invalidUrl: string;
45
45
  notFound: string;
46
+ invalidParams: string;
47
+ taskNotFound: string;
48
+ onlyRenameCompleted: string;
49
+ invalidFilename: string;
50
+ fileNotFound: string;
51
+ fileExists: string;
52
+ renameFailed: string;
53
+ duplicateDownload: string;
46
54
  };
47
55
  success: {
48
56
  configUpdated: string;
@@ -52,6 +60,7 @@ declare const _default: {
52
60
  resumed: string;
53
61
  noResumableTasks: string;
54
62
  deleted: string;
63
+ renamed: string;
55
64
  };
56
65
  };
57
66
  download: {
@@ -45,6 +45,14 @@ exports.default = {
45
45
  accessDenied: '访问被拒绝',
46
46
  invalidUrl: '无效的 url 参数',
47
47
  notFound: '未找到',
48
+ invalidParams: '参数无效',
49
+ taskNotFound: '任务不存在',
50
+ onlyRenameCompleted: '只能重命名已完成且状态正常的任务',
51
+ invalidFilename: '文件名包含非法字符',
52
+ fileNotFound: '文件未找到',
53
+ fileExists: '文件已存在',
54
+ renameFailed: '重命名失败: {error}',
55
+ duplicateDownload: '有 {count} 个 URL 已存在,重复下载',
48
56
  },
49
57
  success: {
50
58
  configUpdated: '配置更新成功',
@@ -54,6 +62,7 @@ exports.default = {
54
62
  resumed: '已恢复 {count} 个下载任务',
55
63
  noResumableTasks: '没有找到可恢复的下载任务',
56
64
  deleted: '已删除 {count} 个下载任务',
65
+ renamed: '重命名成功',
57
66
  },
58
67
  },
59
68
  download: {
package/cjs/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './lib/file-download';
2
2
  export * from './lib/getM3u8Urls';
3
+ export * from './lib/init-proxy';
3
4
  export * from './lib/m3u8-download';
4
5
  export * from './lib/parseM3u8';
5
6
  export * from './video-parser';
package/cjs/index.js CHANGED
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./lib/file-download"), exports);
18
18
  __exportStar(require("./lib/getM3u8Urls"), exports);
19
+ __exportStar(require("./lib/init-proxy"), exports);
19
20
  __exportStar(require("./lib/m3u8-download"), exports);
20
21
  __exportStar(require("./lib/parseM3u8"), exports);
21
22
  __exportStar(require("./video-parser"), exports);
@@ -9,7 +9,7 @@ const i18n_js_1 = require("./i18n.js");
9
9
  const utils_js_1 = require("./utils.js");
10
10
  async function fileDownload(u, opts) {
11
11
  utils_js_1.logger.debug('fileDownload', u, opts);
12
- const { url, options } = (0, format_options_js_1.formatOptions)(u, opts);
12
+ const { url, options } = await (0, format_options_js_1.formatOptions)(u, opts);
13
13
  const lang = (0, i18n_js_1.getLang)(options.lang);
14
14
  const startTime = Date.now();
15
15
  const stats = {
@@ -68,7 +68,7 @@ async function fileDownload(u, opts) {
68
68
  });
69
69
  stats.endTime = Date.now();
70
70
  return {
71
- errmsg: r.filepath ? (0, i18n_js_1.t)('download.status.completed', lang) : (0, i18n_js_1.t)('download.status.failed', lang),
71
+ errmsg: r.filepath ? '' : (0, i18n_js_1.t)('download.status.failed', lang), // t('download.status.completed', lang)
72
72
  ...r,
73
73
  stats,
74
74
  };
@@ -1,6 +1,6 @@
1
1
  import type { M3u8DLOptions } from '../types';
2
- export declare function formatOptions(url: string, opts: M3u8DLOptions): {
2
+ export declare function formatOptions(url: string, opts: M3u8DLOptions): Promise<{
3
3
  url: string;
4
4
  options: M3u8DLOptions;
5
5
  urlMd5: string;
6
- };
6
+ }>;
@@ -47,7 +47,7 @@ const fileSupportExtList = [
47
47
  '.deb',
48
48
  '.rpm',
49
49
  ];
50
- function formatOptions(url, opts) {
50
+ async function formatOptions(url, opts) {
51
51
  const options = {
52
52
  delCache: !opts.debug,
53
53
  saveDir: process.cwd(),
@@ -58,6 +58,18 @@ function formatOptions(url, opts) {
58
58
  if (!options.type) {
59
59
  if (video_parser_1.VideoParser.getPlatform(url).platform !== 'unknown') {
60
60
  options.type = 'parser';
61
+ if (!opts.filename) {
62
+ const info = await video_parser_1.VideoParser.parse(url);
63
+ if (info.code === 0 && info.data?.title) {
64
+ options.filename = info.data.title
65
+ .split('\n')[0]
66
+ // 替换全部的非中英文、数字、下划线为下划线
67
+ .replace(/[^\u4e00-\u9fa5a-zA-Z0-9]+/g, '_')
68
+ .trim()
69
+ .replace(/_+/g, '_')
70
+ .slice(0, 100);
71
+ }
72
+ }
61
73
  }
62
74
  else {
63
75
  options.type = 'm3u8';
@@ -0,0 +1,5 @@
1
+ import type { M3u8DLOptions } from '../types';
2
+ /**
3
+ * 初始化代理
4
+ */
5
+ export declare function initProxy(options: M3u8DLOptions): Promise<void>;
@@ -0,0 +1,93 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.initProxy = initProxy;
37
+ const utils_1 = require("./utils");
38
+ /** 初始化失败 N 次后续忽略代理配置 */
39
+ let initializFailedTimes = 3;
40
+ /**
41
+ * 初始化代理
42
+ */
43
+ async function initProxy(options) {
44
+ if (!initializFailedTimes && !options.force)
45
+ return;
46
+ // 根据代理模式来初始化
47
+ try {
48
+ const g = global;
49
+ let globalAgent = g.GLOBAL_AGENT;
50
+ if (!globalAgent) {
51
+ // 代理未初始化且为禁用状态,则直接返回
52
+ if (options.proxyMode === 'disabled')
53
+ return;
54
+ // 为系统代理模式,但未设置环境变量,则直接返回
55
+ if (options.proxyMode === 'system' && !process.env.HTTP_PROXY && !process.env.HTTPS_PROXY)
56
+ return;
57
+ const globalAgentModule = await Promise.resolve().then(() => __importStar(require('global-agent')));
58
+ const ok = globalAgentModule.bootstrap();
59
+ if (ok)
60
+ globalAgent = g.GLOBAL_AGENT;
61
+ }
62
+ if (options.proxyMode !== 'disabled' && options.noProxy) {
63
+ options.noProxy = options.noProxy.replaceAll('\n', ',').trim();
64
+ }
65
+ if (options.proxyMode === 'custom' && options.proxyUrl) {
66
+ // 自定义代理模式
67
+ globalAgent.HTTP_PROXY = options.proxyUrl;
68
+ globalAgent.HTTPS_PROXY = options.proxyUrl;
69
+ globalAgent.NO_PROXY = options.noProxy;
70
+ utils_1.logger.info('Custom proxy enabled:', options.proxyUrl);
71
+ }
72
+ else if (options.proxyMode === 'disabled') {
73
+ globalAgent.HTTP_PROXY = undefined;
74
+ globalAgent.HTTPS_PROXY = undefined;
75
+ globalAgent.NO_PROXY = undefined;
76
+ // 关闭代理
77
+ utils_1.logger.info('Proxy disabled');
78
+ }
79
+ else {
80
+ // } else if (options.proxyMode === 'system') {
81
+ // 默认为使用系统代理,但支持自定义代理过滤
82
+ globalAgent.HTTP_PROXY = process.env.HTTP_PROXY;
83
+ globalAgent.HTTPS_PROXY = process.env.HTTPS_PROXY;
84
+ globalAgent.NO_PROXY = options.noProxy || process.env.NO_PROXY;
85
+ utils_1.logger.info('System proxy enabled');
86
+ }
87
+ }
88
+ catch (error) {
89
+ utils_1.logger.error('Failed to initialize proxy:', error);
90
+ utils_1.logger.warn('Please install global-agent to enable proxy support: npm install global-agent');
91
+ initializFailedTimes--;
92
+ }
93
+ }
@@ -5,6 +5,6 @@ import type { TsItemInfo } from '../types/m3u8.js';
5
5
  export declare function localPlay(m3u8Info: TsItemInfo[]): Promise<{
6
6
  port: number;
7
7
  origin: string;
8
- server: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
8
+ server: import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>;
9
9
  }>;
10
10
  export declare function toLocalM3u8(m3u8Info: TsItemInfo[], m3u8FilePath?: string, host?: string): string;
@@ -19,7 +19,7 @@ async function localPlay(m3u8Info) {
19
19
  const cacheFilepath = toLocalM3u8(m3u8Info);
20
20
  const filename = (0, node_path_1.basename)(cacheFilepath);
21
21
  const info = await createLocalServer((0, node_path_1.dirname)(cacheDir));
22
- const playUrl = `https://lzw.me/x/m3u8-player?url=${encodeURIComponent(`${info.origin}/${cacheDirname}/${filename}`)}`;
22
+ const playUrl = `https://m3u8-player.lzw.me?from=sdk&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;
@@ -22,15 +22,9 @@ async function m3u8Convert(options, data) {
22
22
  if (process.platform === 'win32')
23
23
  filesAllArr = filesAllArr.map(d => d.replaceAll('\\', '/'));
24
24
  (0, node_fs_1.writeFileSync)(ffconcatFile, `ffconcat version 1.0\n${filesAllArr.join('\n')}`);
25
- let headersString = '';
26
- if (options.headers) {
27
- for (const [key, value] of Object.entries(options.headers)) {
28
- headersString += `-headers "${key}: ${String(value)}" `;
29
- }
30
- }
31
25
  // ffmpeg -i nz.ts -c copy -map 0:v -map 0:a -bsf:a aac_adtstoasc nz.mp4
32
- // const cmd = `"${ffmpegBin}" -async 1 -y -f concat -safe 0 -i "${ffconcatFile}" -acodec copy -vcodec copy -bsf:a aac_adtstoasc ${headersString} "${filepath}"`;
33
- const cmd = `"${ffmpegBin}" -async 1 -y -f concat -safe 0 -i "${ffconcatFile}" -c:v copy -c:a copy -movflags +faststart -fflags +genpts -bsf:a aac_adtstoasc ${headersString} "${filepath}"`;
26
+ // const cmd = `"${ffmpegBin}" -async 1 -y -f concat -safe 0 -i "${ffconcatFile}" -acodec copy -vcodec copy -bsf:a aac_adtstoasc "${filepath}"`;
27
+ const cmd = `"${ffmpegBin}" -async 1 -y -f concat -safe 0 -i "${ffconcatFile}" -c:v copy -c:a copy -movflags +faststart -fflags +genpts -bsf:a aac_adtstoasc "${filepath}"`;
34
28
  utils_1.logger.debug('[convert to mp4]cmd:', (0, console_log_colors_1.cyan)(cmd));
35
29
  const r = (0, fe_utils_1.execSync)(cmd);
36
30
  ffmpegSupport = !r.error;
@@ -90,7 +90,7 @@ exports.workPollPublic = new worker_pool_js_1.WorkerPool(tsDlFile);
90
90
  async function m3u8InfoParse(u, o = {}) {
91
91
  const ffmpegBin = o.ffmpegPath || 'ffmpeg';
92
92
  const ext = (0, utils_js_1.isSupportFfmpeg)(ffmpegBin) ? '.mp4' : '.ts';
93
- const { url, options, urlMd5 } = (0, format_options_js_1.formatOptions)(u, o);
93
+ const { url, options, urlMd5 } = await (0, format_options_js_1.formatOptions)(u, o);
94
94
  /** 最终合并转换后的文件路径 */
95
95
  let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename);
96
96
  if (!filepath.endsWith(ext))
@@ -104,8 +104,9 @@ async function m3u8InfoParse(u, o = {}) {
104
104
  return result;
105
105
  const lang = (0, i18n_js_1.getLang)(o.lang);
106
106
  const m3u8Info = await (0, parseM3u8_js_1.parseM3U8)(url, (0, node_path_1.resolve)(options.cacheDir, urlMd5), options.headers).catch(e => {
107
- utils_js_1.logger.error((0, i18n_js_1.t)('download.status.parseFailed', lang), e.message);
108
- console.log(e);
107
+ utils_js_1.logger.error((0, i18n_js_1.t)('download.status.parseFailed', lang), e?.message);
108
+ if (e)
109
+ console.log(e);
109
110
  });
110
111
  if (m3u8Info && m3u8Info?.tsCount > 0) {
111
112
  result.m3u8Info = m3u8Info;
@@ -66,7 +66,7 @@ async function parseM3U8(content, cacheDir = './cache', headers) {
66
66
  if (!tsKeyInfo.uri.includes('://'))
67
67
  tsKeyInfo.uri = new URL(tsKeyInfo.uri, url).toString();
68
68
  if (tsKeyInfo?.uri && !result.crypto[tsKeyInfo.uri]) {
69
- const r = await (0, utils_1.getRetry)(tsKeyInfo.uri);
69
+ const r = await (0, utils_1.getRetry)(tsKeyInfo.uri, headers);
70
70
  if (r.response.statusCode !== 200) {
71
71
  utils_1.logger.error('获取加密 key 失败:', tsKeyInfo.uri, r.response.statusCode, r.data);
72
72
  }
@@ -5,8 +5,8 @@ export declare const request: Request;
5
5
  export declare const getRetry: <T = string>(url: string, headers?: OutgoingHttpHeaders | string, retries?: number) => Promise<{
6
6
  data: T;
7
7
  buffer: Buffer;
8
- headers: import("http").IncomingHttpHeaders;
9
- response: import("http").IncomingMessage;
8
+ headers: import("node:http").IncomingHttpHeaders;
9
+ response: import("node:http").IncomingMessage;
10
10
  }>;
11
11
  export declare const logger: NLogger;
12
12
  export declare function isSupportFfmpeg(ffmpegBin: string): boolean;
@@ -18,3 +18,5 @@ export declare function getLocation(url: string, method?: string): Promise<strin
18
18
  * 如果 headers 是字符串,会先将其解析为对象;如果 headers 为空,则返回空对象。
19
19
  */
20
20
  export declare function formatHeaders(headers: string | OutgoingHttpHeaders): Record<string, string>;
21
+ /** 异步检查文件是否存在 */
22
+ export declare function checkFileExists(filepath: string): Promise<boolean>;
package/cjs/lib/utils.js CHANGED
@@ -5,7 +5,9 @@ exports.isSupportFfmpeg = isSupportFfmpeg;
5
5
  exports.findFiles = findFiles;
6
6
  exports.getLocation = getLocation;
7
7
  exports.formatHeaders = formatHeaders;
8
+ exports.checkFileExists = checkFileExists;
8
9
  const node_fs_1 = require("node:fs");
10
+ const promises_1 = require("node:fs/promises");
9
11
  const node_path_1 = require("node:path");
10
12
  const fe_utils_1 = require("@lzwme/fe-utils");
11
13
  exports.request = new fe_utils_1.Request({
@@ -61,7 +63,41 @@ async function getLocation(url, method = 'HEAD') {
61
63
  function formatHeaders(headers) {
62
64
  if (!headers)
63
65
  return {};
64
- if (typeof headers === 'string')
65
- headers = Object.fromEntries(headers.split('\n').map(line => line.split(':').map(d => d.trim())));
66
+ if (typeof headers === 'string') {
67
+ headers = headers.trim();
68
+ if (headers.startsWith('{') && headers.endsWith('}')) {
69
+ try {
70
+ headers = JSON.parse(headers);
71
+ }
72
+ catch (e) {
73
+ console.error('解析 headers 失败:', e);
74
+ }
75
+ }
76
+ if (typeof headers === 'string') {
77
+ const parsed = {};
78
+ headers
79
+ .replace(/,\s*([a-zA-Z0-9_-]+:)/g, '\n$1') // 支持如 "Key1: Val1, Key2: Val2" 的格式
80
+ .split('\n')
81
+ .forEach(line => {
82
+ const idx = line.indexOf(':');
83
+ if (idx > 0)
84
+ parsed[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
85
+ });
86
+ headers = parsed;
87
+ }
88
+ }
66
89
  return (0, fe_utils_1.toLowcaseKeyObject)(headers);
67
90
  }
91
+ /** 异步检查文件是否存在 */
92
+ async function checkFileExists(filepath) {
93
+ try {
94
+ if (!filepath)
95
+ return false;
96
+ await (0, promises_1.access)(filepath, promises_1.constants.F_OK);
97
+ return true;
98
+ }
99
+ catch (error) {
100
+ exports.logger.debug('checkFileExists failed:', filepath, error.message);
101
+ return false;
102
+ }
103
+ }
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from 'node:events';
2
- type WorkerPoolCallback<R> = (err: Error | null, result: R, startTime: number) => void;
2
+ type WorkerPoolCallback<R> = (err: Error, result: R, startTime: number) => void;
3
3
  export declare class WorkerPool<T = unknown, R = unknown> extends EventEmitter {
4
4
  private processorFile;
5
5
  numThreads: number;
@@ -38,7 +38,7 @@ async function formatUrls(urls, options) {
38
38
  continue;
39
39
  }
40
40
  }
41
- const r = (0, format_options_1.formatOptions)(url, options);
41
+ const r = await (0, format_options_1.formatOptions)(url, options);
42
42
  taskset.set(r.url, r.options);
43
43
  }
44
44
  return taskset;
@@ -88,8 +88,7 @@ async function m3u8BatchDownload(urls, options) {
88
88
  }
89
89
  };
90
90
  if (o.type === 'parser') {
91
- const vp = new video_parser_1.VideoParser();
92
- vp.download(url, o).then(r => afterDownload(r, url));
91
+ video_parser_1.VideoParser.download(url, o).then(r => afterDownload(r, url));
93
92
  }
94
93
  else if (o.type === 'file') {
95
94
  (0, file_download_1.fileDownload)(url, o).then(r => afterDownload(r, url));
@@ -1,5 +1,5 @@
1
1
  import type { Express } from 'express';
2
- import type { Server } from 'ws';
2
+ import type { WebSocketServer } from 'ws';
3
3
  import type { M3u8DLOptions, M3u8DLProgressStats, M3u8WorkerPool, TsItemInfo } from '../types/m3u8.js';
4
4
  interface DLServerOptions {
5
5
  port?: number;
@@ -18,12 +18,18 @@ interface CacheItem extends Partial<M3u8DLProgressStats> {
18
18
  /** 格式化后实际下载使用的参数 */
19
19
  dlOptions?: M3u8DLOptions;
20
20
  status: 'pause' | 'resume' | 'done' | 'pending' | 'error';
21
+ /**
22
+ * 当前任务的 ts 信息
23
+ * @deprecated 下一版本将移除
24
+ */
21
25
  current?: TsItemInfo;
26
+ /** 当前任务的 ts 缓存目录 */
27
+ cacheDir?: string;
22
28
  workPoll?: M3u8WorkerPool;
23
29
  }
24
30
  export declare class DLServer {
25
31
  app: Express;
26
- wss: Server;
32
+ wss: WebSocketServer | null;
27
33
  /** DS 参数 */
28
34
  options: DLServerOptions;
29
35
  private serverInfo;
@@ -38,11 +44,12 @@ export declare class DLServer {
38
44
  private checkDLFileLaest;
39
45
  private checkDLFileTimer;
40
46
  private checkDLFileIsExists;
47
+ private checkItemStatus;
41
48
  dlCacheClone(): [string, CacheItem][];
42
49
  private cacheSaveTimer;
43
50
  saveCache(): void;
44
- private readConfig;
45
- saveConfig(config: M3u8DLOptions, configPath?: string): void;
51
+ private loadConfig;
52
+ saveConfig(config: M3u8DLOptions, configPath?: string): Promise<void>;
46
53
  private createApp;
47
54
  private startDownload;
48
55
  startNextPending(): void;