@lzwme/m3u8-dl 1.4.2 → 1.5.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/cjs/cli.js +1 -0
- package/cjs/lib/m3u8-convert.js +10 -4
- package/cjs/lib/m3u8-download.js +6 -1
- package/cjs/lib/utils.d.ts +1 -1
- package/cjs/lib/utils.js +2 -2
- package/cjs/server/download-server.js +2 -10
- package/cjs/types/m3u8.d.ts +2 -0
- package/client/index.html +5 -15
- package/package.json +12 -11
package/cjs/cli.js
CHANGED
|
@@ -65,6 +65,7 @@ commander_1.program
|
|
|
65
65
|
.option('-S, --save-dir <dirpath>', '下载文件保存的路径。默认为当前目录')
|
|
66
66
|
.option('--no-del-cache', '下载成功后是否删除临时文件。默认为 true。保存临时文件可以在重复下载时识别缓存')
|
|
67
67
|
.option('--no-convert', '下载成功后,是否不合并转换为 mp4 文件。默认为 true。')
|
|
68
|
+
.option('--use-global-ffmpeg', '是否使用系统安装的 ffmpeg 而不是内置的 ffmpeg-static')
|
|
68
69
|
.option('-H, --headers <headers>', '自定义请求头。格式为 key1=value1\nkey2=value2')
|
|
69
70
|
.option('-T, --type <type>', '指定下载类型。默认根据URL自动识别,如果是批量下载多个不同 URL 类型,请不要设置。可选值:m3u8, file, parser')
|
|
70
71
|
.option('-I, --ignore-segments <time-segments>', '忽略的视频片段,用-分割起始时间点,多个用逗号分隔。如:0-10,20-30')
|
package/cjs/lib/m3u8-convert.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.m3u8Convert = m3u8Convert;
|
|
4
7
|
const node_fs_1 = require("node:fs");
|
|
5
8
|
const node_path_1 = require("node:path");
|
|
6
9
|
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
7
10
|
const console_log_colors_1 = require("console-log-colors");
|
|
11
|
+
const ffmpeg_static_1 = __importDefault(require("ffmpeg-static"));
|
|
8
12
|
const utils_1 = require("./utils");
|
|
9
13
|
async function m3u8Convert(options, data) {
|
|
10
|
-
|
|
14
|
+
const useGlobal = options.useGlobalFfmpeg || false;
|
|
15
|
+
const ffmpegBin = useGlobal ? 'ffmpeg' : ffmpeg_static_1.default;
|
|
16
|
+
let ffmpegSupport = (0, utils_1.isSupportFfmpeg)(ffmpegBin);
|
|
11
17
|
let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename);
|
|
12
18
|
if (!ffmpegSupport)
|
|
13
19
|
filepath = filepath.replace(/\.mp4$/, '.ts');
|
|
@@ -28,13 +34,13 @@ async function m3u8Convert(options, data) {
|
|
|
28
34
|
}
|
|
29
35
|
}
|
|
30
36
|
// ffmpeg -i nz.ts -c copy -map 0:v -map 0:a -bsf:a aac_adtstoasc nz.mp4
|
|
31
|
-
// const cmd = `
|
|
32
|
-
const cmd = `
|
|
37
|
+
// const cmd = `"${ffmpegBin}" -async 1 -y -f concat -safe 0 -i "${ffconcatFile}" -acodec copy -vcodec copy -bsf:a aac_adtstoasc ${headersString} "${filepath}"`;
|
|
38
|
+
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}"`;
|
|
33
39
|
utils_1.logger.debug('[convert to mp4]cmd:', (0, console_log_colors_1.cyan)(cmd));
|
|
34
40
|
const r = (0, fe_utils_1.execSync)(cmd);
|
|
35
41
|
ffmpegSupport = !r.error;
|
|
36
42
|
if (r.error)
|
|
37
|
-
utils_1.logger.error(
|
|
43
|
+
utils_1.logger.error(`Conversion to mp4 failed. Please confirm that \`${useGlobal ? 'ffmpeg' : 'ffmpeg-static'}\` is ${useGlobal ? 'installed' : 'available'}!`, r.stderr);
|
|
38
44
|
else
|
|
39
45
|
(0, node_fs_1.unlinkSync)(ffconcatFile);
|
|
40
46
|
}
|
package/cjs/lib/m3u8-download.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.workPollPublic = exports.downloadQueue = exports.DownloadQueue = void 0;
|
|
4
7
|
exports.preDownLoad = preDownLoad;
|
|
@@ -9,6 +12,7 @@ const node_path_1 = require("node:path");
|
|
|
9
12
|
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
10
13
|
const helper_1 = require("@lzwme/fe-utils/cjs/common/helper");
|
|
11
14
|
const console_log_colors_1 = require("console-log-colors");
|
|
15
|
+
const ffmpeg_static_1 = __importDefault(require("ffmpeg-static"));
|
|
12
16
|
const format_options_js_1 = require("./format-options.js");
|
|
13
17
|
const local_play_js_1 = require("./local-play.js");
|
|
14
18
|
const m3u8_convert_js_1 = require("./m3u8-convert.js");
|
|
@@ -87,8 +91,9 @@ const cache = {
|
|
|
87
91
|
const tsDlFile = (0, node_path_1.resolve)(__dirname, './ts-download.js');
|
|
88
92
|
exports.workPollPublic = new worker_pool_js_1.WorkerPool(tsDlFile);
|
|
89
93
|
async function m3u8InfoParse(u, o = {}) {
|
|
94
|
+
const ffmpegBin = o.useGlobalFfmpeg ? 'ffmpeg' : ffmpeg_static_1.default;
|
|
95
|
+
const ext = (0, utils_js_1.isSupportFfmpeg)(ffmpegBin) ? '.mp4' : '.ts';
|
|
90
96
|
const { url, options, urlMd5 } = (0, format_options_js_1.formatOptions)(u, o);
|
|
91
|
-
const ext = (0, utils_js_1.isSupportFfmpeg)() ? '.mp4' : '.ts';
|
|
92
97
|
/** 最终合并转换后的文件路径 */
|
|
93
98
|
let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename);
|
|
94
99
|
if (!filepath.endsWith(ext))
|
package/cjs/lib/utils.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export declare const getRetry: <T = string>(url: string, headers?: OutgoingHttpH
|
|
|
9
9
|
response: import("http").IncomingMessage;
|
|
10
10
|
}>;
|
|
11
11
|
export declare const logger: NLogger;
|
|
12
|
-
export declare function isSupportFfmpeg(): boolean;
|
|
12
|
+
export declare function isSupportFfmpeg(ffmpegBin: string): boolean;
|
|
13
13
|
export declare function findFiles(apidir?: string, validate?: (filepath: string, stat: Stats) => boolean): string[];
|
|
14
14
|
/** 获取重定向后的 URL */
|
|
15
15
|
export declare function getLocation(url: string, method?: string): Promise<string>;
|
package/cjs/lib/utils.js
CHANGED
|
@@ -24,9 +24,9 @@ const getRetry = (url, headers, retries = 3) => (0, fe_utils_1.retry)(() => expo
|
|
|
24
24
|
exports.getRetry = getRetry;
|
|
25
25
|
exports.logger = fe_utils_1.NLogger.getLogger('[M3U8-DL]', { color: fe_utils_1.color });
|
|
26
26
|
let _isSupportFfmpeg = null;
|
|
27
|
-
function isSupportFfmpeg() {
|
|
27
|
+
function isSupportFfmpeg(ffmpegBin) {
|
|
28
28
|
if (null == _isSupportFfmpeg)
|
|
29
|
-
_isSupportFfmpeg = (0, fe_utils_1.execSync)(
|
|
29
|
+
_isSupportFfmpeg = (0, fe_utils_1.execSync)(`${ffmpegBin} -version`).stderr === '';
|
|
30
30
|
return _isSupportFfmpeg;
|
|
31
31
|
}
|
|
32
32
|
function findFiles(apidir, validate) {
|
|
@@ -537,21 +537,13 @@ class DLServer {
|
|
|
537
537
|
'Access-Control-Allow-Headers': '*',
|
|
538
538
|
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
|
|
539
539
|
'Access-Control-Allow-Origin': '*',
|
|
540
|
+
'Access-Control-Allow-Credentials': 'true',
|
|
540
541
|
'Content-Length': String(stats.size),
|
|
541
542
|
'Content-Type': ext === 'ts' ? 'video/mp2t' : ext === 'm3u8' ? 'application/vnd.apple.mpegurl' : ext === 'mp4' ? 'video/mp4' : 'text/plain',
|
|
542
543
|
});
|
|
543
544
|
res.setHeaders(headers);
|
|
544
545
|
if (ext === 'm3u8' || ('ts' === ext && stats.size < 1024 * 1024 * 3)) {
|
|
545
|
-
|
|
546
|
-
if (ext === 'm3u8') {
|
|
547
|
-
const baseDirName = (0, node_path_1.basename)(filepath, '.m3u8');
|
|
548
|
-
content = content
|
|
549
|
-
.toString('utf8')
|
|
550
|
-
.split('\n')
|
|
551
|
-
.map(line => (line.endsWith('.ts') && !line.includes('/') ? `${baseDirName}/${line}` : line))
|
|
552
|
-
.join('\n');
|
|
553
|
-
}
|
|
554
|
-
res.send(content);
|
|
546
|
+
res.send((0, node_fs_1.readFileSync)(filepath));
|
|
555
547
|
utils_js_1.logger.debug('[Localplay]file sent:', (0, console_log_colors_1.gray)(filepath), 'Size:', stats.size, 'bytes');
|
|
556
548
|
}
|
|
557
549
|
else {
|
package/cjs/types/m3u8.d.ts
CHANGED
|
@@ -140,6 +140,8 @@ export interface M3u8DLOptions {
|
|
|
140
140
|
* - 'parser':下载 VideoParser 支持解析的平台视频文件
|
|
141
141
|
*/
|
|
142
142
|
type?: 'm3u8' | 'file' | 'parser';
|
|
143
|
+
/** 是否使用系统安装的 ffmpeg 而不是内置的 ffmpeg-static。默认为 false */
|
|
144
|
+
useGlobalFfmpeg?: boolean;
|
|
143
145
|
}
|
|
144
146
|
export interface M3u8DLResult extends Partial<DownloadResult> {
|
|
145
147
|
/** 下载进度统计 */
|
package/client/index.html
CHANGED
|
@@ -744,7 +744,7 @@ services:
|
|
|
744
744
|
this.ws.close();
|
|
745
745
|
}
|
|
746
746
|
|
|
747
|
-
const ws = new WebSocket(
|
|
747
|
+
const ws = new WebSocket(`${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws?token=${this.token}`);
|
|
748
748
|
this.ws = ws;
|
|
749
749
|
ws.onmessage = (e) => {
|
|
750
750
|
let { type, data } = T.safeJSONParse(e.data);
|
|
@@ -832,7 +832,7 @@ services:
|
|
|
832
832
|
html: `
|
|
833
833
|
<div class="text-left">
|
|
834
834
|
<div class="flex flex-row gap-4">
|
|
835
|
-
<input type="text" id="playUrl" placeholder="[实验性]
|
|
835
|
+
<input type="text" id="playUrl" placeholder="[实验性]输入视频播放页地址,尝试提取m3u8下载链接" autocomplete="off" id="urlInput" class="flex-1 border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400" value="">
|
|
836
836
|
<div class="flex flex-row gap-1">
|
|
837
837
|
<button type="button" id="getM3u8UrlsBtn" class="player-btn px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none">
|
|
838
838
|
提取
|
|
@@ -840,13 +840,6 @@ services:
|
|
|
840
840
|
</div>
|
|
841
841
|
</div>
|
|
842
842
|
|
|
843
|
-
<div class="mt-4">
|
|
844
|
-
<div class="flex flex-row gap-2 items-center">
|
|
845
|
-
<input id="subUrlRegex" class="flex-1 p-2 border rounded-lg focus:border-blue-500" placeholder="[实验性](可选)播放页链接特征规则">
|
|
846
|
-
</div>
|
|
847
|
-
<p class="ml-2 mt-1 text-sm text-gray-500">用于从视频列表页准确识别播放地址。如:<code>play/845-1-</code></p>
|
|
848
|
-
</div>
|
|
849
|
-
|
|
850
843
|
<div class="mt-4">
|
|
851
844
|
<label class="block text-sm font-bold text-gray-700 mb-1">视频链接(每行一个,支持m3u8地址及抖音、微博、皮皮虾视频分享链接)</label>
|
|
852
845
|
<textarea id="downloadUrls" class="w-full p-2 border rounded-lg focus:ring-blue-500" rows="3" placeholder="格式: URL | 名称(可选)、名称 | URL"></textarea>
|
|
@@ -937,9 +930,7 @@ services:
|
|
|
937
930
|
btn.setAttribute('disabled', 'disabled');
|
|
938
931
|
btn.innerText = '解析中...';
|
|
939
932
|
|
|
940
|
-
|
|
941
|
-
const subUrlRegex = document.getElementById('subUrlRegex').value.trim();
|
|
942
|
-
T.post('/api/getM3u8Urls', { url, headers, subUrlRegex }).then(r => {
|
|
933
|
+
T.post('/api/getM3u8Urls', { url, headers: document.getElementById('headers').value.trim() }).then(r => {
|
|
943
934
|
if (Array.isArray(r.data)) {
|
|
944
935
|
document.getElementById('downloadUrls').value = r.data.map(d => d.join(' | ')).join('\n');
|
|
945
936
|
T.toast(r.message || `解析完成!获取到 ${r.data.length} 个地址`);
|
|
@@ -1073,11 +1064,10 @@ services:
|
|
|
1073
1064
|
},
|
|
1074
1065
|
/** 边下边播 */
|
|
1075
1066
|
localPlay: function (task) {
|
|
1076
|
-
const
|
|
1077
|
-
const url = location.origin + `/localplay/${encodeURIComponent(filepath)}`;
|
|
1067
|
+
const url = location.origin + `/localplay/${encodeURIComponent(task.localVideo || '') || task.localM3u8}`;
|
|
1078
1068
|
console.log(task);
|
|
1079
1069
|
Swal.fire({
|
|
1080
|
-
title: task?.
|
|
1070
|
+
title: task.options?.filename || task.url,
|
|
1081
1071
|
width: '1000px',
|
|
1082
1072
|
padding: 0,
|
|
1083
1073
|
allowOutsideClick: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lzwme/m3u8-dl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Batch download of m3u8 files and convert to mp4",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"types": "cjs/index.d.ts",
|
|
@@ -44,24 +44,24 @@
|
|
|
44
44
|
"registry": "https://registry.npmjs.com"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@biomejs/biome": "^2.
|
|
48
|
-
"@eslint/js": "^9.
|
|
47
|
+
"@biomejs/biome": "^2.2.2",
|
|
48
|
+
"@eslint/js": "^9.34.0",
|
|
49
49
|
"@lzwme/fed-lint-helper": "^2.6.6",
|
|
50
50
|
"@types/express": "^5.0.3",
|
|
51
|
-
"@types/m3u8-parser": "^7.2.
|
|
52
|
-
"@types/node": "^24.0
|
|
51
|
+
"@types/m3u8-parser": "^7.2.3",
|
|
52
|
+
"@types/node": "^24.3.0",
|
|
53
53
|
"@types/ws": "^8.18.1",
|
|
54
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
55
|
-
"@typescript-eslint/parser": "^8.
|
|
56
|
-
"eslint": "^9.
|
|
54
|
+
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
|
55
|
+
"@typescript-eslint/parser": "^8.41.0",
|
|
56
|
+
"eslint": "^9.34.0",
|
|
57
57
|
"eslint-config-prettier": "^10.1.8",
|
|
58
|
-
"eslint-plugin-prettier": "^5.5.
|
|
58
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
59
59
|
"express": "^5.1.0",
|
|
60
60
|
"husky": "^9.1.7",
|
|
61
61
|
"prettier": "^3.6.2",
|
|
62
62
|
"standard-version": "^9.5.0",
|
|
63
|
-
"typescript": "^5.
|
|
64
|
-
"typescript-eslint": "^8.
|
|
63
|
+
"typescript": "^5.9.2",
|
|
64
|
+
"typescript-eslint": "^8.41.0",
|
|
65
65
|
"ws": "^8.18.3"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"commander": "^14.0.0",
|
|
70
70
|
"console-log-colors": "^0.5.0",
|
|
71
71
|
"enquirer": "^2.4.1",
|
|
72
|
+
"ffmpeg-static": "^5.2.0",
|
|
72
73
|
"m3u8-parser": "^7.2.0"
|
|
73
74
|
},
|
|
74
75
|
"files": [
|