@lzwme/m3u8-dl 1.4.1 → 1.4.3
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/lib/file-download.js +1 -1
- package/cjs/lib/getM3u8Urls.d.ts +9 -1
- package/cjs/lib/getM3u8Urls.js +54 -23
- package/cjs/server/download-server.js +7 -15
- package/client/index.html +3 -4
- package/package.json +12 -12
package/cjs/lib/file-download.js
CHANGED
package/cjs/lib/getM3u8Urls.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import type { OutgoingHttpHeaders } from 'node:http';
|
|
2
|
+
export interface GetM3u8UrlsOption {
|
|
3
|
+
url: string;
|
|
4
|
+
/** 播放子页面 URL 特征规则 */
|
|
5
|
+
subUrlRegex?: string | RegExp;
|
|
6
|
+
headers?: OutgoingHttpHeaders | string;
|
|
7
|
+
deep?: number;
|
|
8
|
+
visited?: Set<string>;
|
|
9
|
+
}
|
|
2
10
|
/** 从指定的 url 页面中提取 m3u8 播放地址。deep 指定搜索页面深度 */
|
|
3
|
-
export declare function getM3u8Urls(
|
|
11
|
+
export declare function getM3u8Urls(opts: GetM3u8UrlsOption): Promise<Map<string, string>>;
|
package/cjs/lib/getM3u8Urls.js
CHANGED
|
@@ -3,67 +3,98 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getM3u8Urls = getM3u8Urls;
|
|
4
4
|
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
5
5
|
const utils_js_1 = require("./utils.js");
|
|
6
|
+
function getFormatTitle(text) {
|
|
7
|
+
if (typeof text !== 'string' || !text)
|
|
8
|
+
return '';
|
|
9
|
+
if (/^\d+$/.test(text))
|
|
10
|
+
return text;
|
|
11
|
+
const match = /第(\d+)(集|期)/.exec(text);
|
|
12
|
+
if (match)
|
|
13
|
+
return match[0];
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
6
16
|
/** 从指定的 url 页面中提取 m3u8 播放地址。deep 指定搜索页面深度 */
|
|
7
|
-
async function getM3u8Urls(
|
|
8
|
-
const
|
|
9
|
-
const
|
|
17
|
+
async function getM3u8Urls(opts) {
|
|
18
|
+
const options = { headers: {}, deep: 1, visited: new Set(), ...opts };
|
|
19
|
+
const baseUrl = new URL(options.url).origin;
|
|
20
|
+
const req = new fe_utils_1.Request({
|
|
21
|
+
headers: { 'content-type': 'text/html; charset=UTF-8', referer: baseUrl, ...(0, utils_js_1.formatHeaders)(options.headers) },
|
|
22
|
+
reqOptions: { rejectUnauthorized: false },
|
|
23
|
+
});
|
|
24
|
+
const { data: html, response } = await req.get(options.url);
|
|
10
25
|
const m3u8Urls = new Map();
|
|
11
26
|
if (!response.statusCode || response.statusCode >= 400) {
|
|
12
|
-
utils_js_1.logger.error('获取页面失败:', fe_utils_1.color.red(url), response.statusCode, response.statusMessage, html);
|
|
27
|
+
utils_js_1.logger.error('获取页面失败:', fe_utils_1.color.red(options.url), response.statusCode, response.statusMessage, html);
|
|
13
28
|
return m3u8Urls;
|
|
14
29
|
}
|
|
15
30
|
// 从 html 中正则匹配提取 m3u8
|
|
16
|
-
const m3u8Regex = /https?:[^\s'"]+\.m3u8(\?[^\s'"]*)?/gi;
|
|
31
|
+
const m3u8Regex = /https?:[^\s'":]+\.(m3u8|mp4)(\?[^\s'"]*)?/gi;
|
|
17
32
|
// 1. 直接正则匹配 m3u8 地址
|
|
18
33
|
let match = m3u8Regex.exec(html);
|
|
19
|
-
const title = (/<title>([^<]+)</.exec(html)?.[1].split('-')[0] || '').replace(
|
|
34
|
+
const title = (/<title>([^<]+)</.exec(html)?.[1].split('-')[0] || '').replace(/在线播放|在线观看|详情|介绍|《|》/g, '').trim();
|
|
20
35
|
while (match) {
|
|
21
36
|
const href = match[0].replaceAll('\\/', '/');
|
|
22
37
|
match = m3u8Regex.exec(html);
|
|
23
38
|
if (!m3u8Urls.has(href))
|
|
24
|
-
m3u8Urls.set(href, title);
|
|
39
|
+
m3u8Urls.set(href, getFormatTitle(title) || title);
|
|
25
40
|
}
|
|
26
41
|
// 找到了多个链接,修改 title 添加序号
|
|
27
|
-
if (m3u8Urls.size > 3 &&
|
|
42
|
+
if (m3u8Urls.size > 3 && !/第\d+(集|期)/.test(title)) {
|
|
28
43
|
let idx = 1;
|
|
29
44
|
for (const [key] of m3u8Urls) {
|
|
30
|
-
m3u8Urls.set(key, `${title}第${String(idx
|
|
45
|
+
m3u8Urls.set(key, `${title}第${String(++idx).padStart(2, '0')}集`);
|
|
31
46
|
}
|
|
32
47
|
}
|
|
33
48
|
// 2. 若未找到且深度大于 0,则获取所有 a 标签的 href 并递归查找
|
|
34
|
-
if (m3u8Urls.size === 0 && deep > 0) {
|
|
35
|
-
utils_js_1.logger.debug('未获取到 m3u8 地址', fe_utils_1.color.gray(url), html.length);
|
|
36
|
-
visited.add(url);
|
|
49
|
+
if (m3u8Urls.size === 0 && options.deep > 0) {
|
|
50
|
+
utils_js_1.logger.debug('未获取到 m3u8 地址', fe_utils_1.color.gray(options.url), html.length);
|
|
51
|
+
options.visited.add(options.url);
|
|
37
52
|
const aTagRegex = /<a\s+[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi;
|
|
38
53
|
let aMatch = aTagRegex.exec(html);
|
|
39
|
-
const origin = new URL(url).origin;
|
|
40
54
|
const subPageUrls = new Map();
|
|
41
55
|
let failedSubPages = 0;
|
|
42
56
|
while (aMatch) {
|
|
43
|
-
const href = aMatch[1] ? new URL(aMatch[1],
|
|
57
|
+
const href = aMatch[1] ? new URL(aMatch[1], baseUrl).toString() : '';
|
|
44
58
|
const text = aMatch[2].replace(/<[^>]+>/g, '');
|
|
45
59
|
aMatch = aTagRegex.exec(html);
|
|
46
|
-
if (!href || visited.has(href) || !href.startsWith(
|
|
60
|
+
if (!href || options.visited.has(href) || !href.startsWith(baseUrl))
|
|
47
61
|
continue;
|
|
48
|
-
if (
|
|
62
|
+
if (options.subUrlRegex) {
|
|
63
|
+
if (typeof options.subUrlRegex === 'string') {
|
|
64
|
+
options.subUrlRegex = new RegExp(options.subUrlRegex.replaceAll(/\*+/g, '.+'));
|
|
65
|
+
}
|
|
66
|
+
if (!options.subUrlRegex.test(href))
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
else if (!/集|期|HD|高清|抢先|BD/.test(text))
|
|
49
70
|
continue;
|
|
50
71
|
subPageUrls.set(href, text);
|
|
51
|
-
utils_js_1.logger.debug(' >
|
|
72
|
+
utils_js_1.logger.debug(' > 提取到子页面: ', fe_utils_1.color.gray(href), text);
|
|
52
73
|
}
|
|
53
74
|
for (const [href, text] of subPageUrls) {
|
|
54
75
|
try {
|
|
55
|
-
visited.add(href);
|
|
56
|
-
const subUrls = await getM3u8Urls(
|
|
57
|
-
utils_js_1.logger.debug(' > 从子页面提取: ', fe_utils_1.color.gray(href), text, subUrls
|
|
76
|
+
options.visited.add(href);
|
|
77
|
+
const subUrls = await getM3u8Urls({ ...options, url: href, deep: options.deep - 1 });
|
|
78
|
+
utils_js_1.logger.debug(' > 从子页面提取: ', fe_utils_1.color.gray(href), text, subUrls);
|
|
58
79
|
if (subUrls.size === 0 && m3u8Urls.size === 0) {
|
|
59
80
|
failedSubPages++;
|
|
60
81
|
if (failedSubPages > 3) {
|
|
61
|
-
utils_js_1.logger.warn(`连续查找 ${failedSubPages} 个子页面均未获取到,不再继续`, url, href);
|
|
82
|
+
utils_js_1.logger.warn(`连续查找 ${failedSubPages} 个子页面均未获取到,不再继续`, options.url, href);
|
|
62
83
|
return m3u8Urls;
|
|
63
84
|
}
|
|
64
85
|
}
|
|
65
|
-
for (const [u, t] of subUrls)
|
|
66
|
-
|
|
86
|
+
for (const [u, t] of subUrls) {
|
|
87
|
+
let stitle = t;
|
|
88
|
+
for (const s of [text, t, m3u8Urls.get(u) || '']) {
|
|
89
|
+
const ft = getFormatTitle(s);
|
|
90
|
+
if (ft) {
|
|
91
|
+
stitle = ft;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
utils_js_1.logger.debug(' > m3u8地址: ', fe_utils_1.color.gray(u), fe_utils_1.color.green(stitle));
|
|
96
|
+
m3u8Urls.set(u, stitle.trim());
|
|
97
|
+
}
|
|
67
98
|
}
|
|
68
99
|
catch (err) {
|
|
69
100
|
utils_js_1.logger.warn(' > 尝试访问子页面异常: ', fe_utils_1.color.red(href), err.message);
|
|
@@ -55,7 +55,7 @@ class DLServer {
|
|
|
55
55
|
cacheDir: process.env.DS_CACHE_DIR || (0, node_path_1.resolve)((0, node_os_1.homedir)(), '.m3u8-dl/cache'),
|
|
56
56
|
token: process.env.DS_SECRET || process.env.DS_TOKEN || '',
|
|
57
57
|
debug: process.env.DS_DEBUG === '1',
|
|
58
|
-
limitFileAccess:
|
|
58
|
+
limitFileAccess: ['1', 'true'].includes(process.env.DS_LIMTE_FILE_ACCESS),
|
|
59
59
|
};
|
|
60
60
|
serverInfo = {
|
|
61
61
|
version: '',
|
|
@@ -198,7 +198,7 @@ class DLServer {
|
|
|
198
198
|
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(rootDir, 'client/local/cdn'))) {
|
|
199
199
|
indexHtml = indexHtml
|
|
200
200
|
.replaceAll('https://s4.zstatic.net/ajax/libs', 'local/cdn')
|
|
201
|
-
.replaceAll(/integrity="
|
|
201
|
+
.replaceAll(/integrity="[^"]+"\n?/g, '')
|
|
202
202
|
.replace('https://cdn.tailwindcss.com/3.4.16', 'local/cdn/tailwindcss/3.4.16/tailwindcss.min.js');
|
|
203
203
|
}
|
|
204
204
|
res.setHeader('content-type', 'text/html').send(indexHtml);
|
|
@@ -253,7 +253,7 @@ class DLServer {
|
|
|
253
253
|
if (!url)
|
|
254
254
|
return utils_js_1.logger.error('[satartDownload]Invalid URL:', url);
|
|
255
255
|
if (url.endsWith('.html')) {
|
|
256
|
-
const item = Array.from(await (0, getM3u8Urls_js_1.getM3u8Urls)(url, options.headers))[0];
|
|
256
|
+
const item = Array.from(await (0, getM3u8Urls_js_1.getM3u8Urls)({ url, headers: options.headers }))[0];
|
|
257
257
|
if (!item)
|
|
258
258
|
return utils_js_1.logger.error('[startDownload]不是有效(包含)M3U8的地址:', url);
|
|
259
259
|
url = item[0];
|
|
@@ -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 {
|
|
@@ -564,12 +556,12 @@ class DLServer {
|
|
|
564
556
|
res.status(404).send({ message: 'Not Found', code: 404 });
|
|
565
557
|
});
|
|
566
558
|
app.post('/api/getM3u8Urls', (req, res) => {
|
|
567
|
-
const { url, headers } = req.body;
|
|
559
|
+
const { url, headers, subUrlRegex } = req.body;
|
|
568
560
|
if (!url) {
|
|
569
561
|
res.json({ code: 1001, message: '无效的 url 参数' });
|
|
570
562
|
}
|
|
571
563
|
else {
|
|
572
|
-
(0, getM3u8Urls_js_1.getM3u8Urls)(url, headers)
|
|
564
|
+
(0, getM3u8Urls_js_1.getM3u8Urls)({ url, headers, subUrlRegex })
|
|
573
565
|
.then(d => res.json({ code: 0, data: Array.from(d) }))
|
|
574
566
|
.catch(err => res.json({ code: 401, message: err.message }));
|
|
575
567
|
}
|
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);
|
|
@@ -1064,11 +1064,10 @@ services:
|
|
|
1064
1064
|
},
|
|
1065
1065
|
/** 边下边播 */
|
|
1066
1066
|
localPlay: function (task) {
|
|
1067
|
-
const
|
|
1068
|
-
const url = location.origin + `/localplay/${encodeURIComponent(filepath)}`;
|
|
1067
|
+
const url = location.origin + `/localplay/${encodeURIComponent(task.localVideo || '') || task.localM3u8}`;
|
|
1069
1068
|
console.log(task);
|
|
1070
1069
|
Swal.fire({
|
|
1071
|
-
title: task?.
|
|
1070
|
+
title: task.options?.filename || task.url,
|
|
1072
1071
|
width: '1000px',
|
|
1073
1072
|
padding: 0,
|
|
1074
1073
|
allowOutsideClick: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lzwme/m3u8-dl",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
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.
|
|
57
|
-
"eslint-config-prettier": "^10.1.
|
|
58
|
-
"eslint-plugin-prettier": "^5.5.
|
|
54
|
+
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
|
55
|
+
"@typescript-eslint/parser": "^8.41.0",
|
|
56
|
+
"eslint": "^9.34.0",
|
|
57
|
+
"eslint-config-prettier": "^10.1.8",
|
|
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": {
|