@lzwme/m3u8-dl 1.4.1 → 1.4.2
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 +5 -5
- package/client/index.html +11 -2
- package/package.json +10 -10
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];
|
|
@@ -564,12 +564,12 @@ class DLServer {
|
|
|
564
564
|
res.status(404).send({ message: 'Not Found', code: 404 });
|
|
565
565
|
});
|
|
566
566
|
app.post('/api/getM3u8Urls', (req, res) => {
|
|
567
|
-
const { url, headers } = req.body;
|
|
567
|
+
const { url, headers, subUrlRegex } = req.body;
|
|
568
568
|
if (!url) {
|
|
569
569
|
res.json({ code: 1001, message: '无效的 url 参数' });
|
|
570
570
|
}
|
|
571
571
|
else {
|
|
572
|
-
(0, getM3u8Urls_js_1.getM3u8Urls)(url, headers)
|
|
572
|
+
(0, getM3u8Urls_js_1.getM3u8Urls)({ url, headers, subUrlRegex })
|
|
573
573
|
.then(d => res.json({ code: 0, data: Array.from(d) }))
|
|
574
574
|
.catch(err => res.json({ code: 401, message: err.message }));
|
|
575
575
|
}
|
package/client/index.html
CHANGED
|
@@ -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,6 +840,13 @@ 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
|
+
|
|
843
850
|
<div class="mt-4">
|
|
844
851
|
<label class="block text-sm font-bold text-gray-700 mb-1">视频链接(每行一个,支持m3u8地址及抖音、微博、皮皮虾视频分享链接)</label>
|
|
845
852
|
<textarea id="downloadUrls" class="w-full p-2 border rounded-lg focus:ring-blue-500" rows="3" placeholder="格式: URL | 名称(可选)、名称 | URL"></textarea>
|
|
@@ -930,7 +937,9 @@ services:
|
|
|
930
937
|
btn.setAttribute('disabled', 'disabled');
|
|
931
938
|
btn.innerText = '解析中...';
|
|
932
939
|
|
|
933
|
-
|
|
940
|
+
const headers = document.getElementById('headers').value.trim();
|
|
941
|
+
const subUrlRegex = document.getElementById('subUrlRegex').value.trim();
|
|
942
|
+
T.post('/api/getM3u8Urls', { url, headers, subUrlRegex }).then(r => {
|
|
934
943
|
if (Array.isArray(r.data)) {
|
|
935
944
|
document.getElementById('downloadUrls').value = r.data.map(d => d.join(' | ')).join('\n');
|
|
936
945
|
T.toast(r.message || `解析完成!获取到 ${r.data.length} 个地址`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lzwme/m3u8-dl",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
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.1.
|
|
48
|
-
"@eslint/js": "^9.
|
|
47
|
+
"@biomejs/biome": "^2.1.2",
|
|
48
|
+
"@eslint/js": "^9.31.0",
|
|
49
49
|
"@lzwme/fed-lint-helper": "^2.6.6",
|
|
50
50
|
"@types/express": "^5.0.3",
|
|
51
51
|
"@types/m3u8-parser": "^7.2.2",
|
|
52
|
-
"@types/node": "^24.0.
|
|
52
|
+
"@types/node": "^24.0.15",
|
|
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.37.0",
|
|
55
|
+
"@typescript-eslint/parser": "^8.37.0",
|
|
56
|
+
"eslint": "^9.31.0",
|
|
57
|
+
"eslint-config-prettier": "^10.1.8",
|
|
58
|
+
"eslint-plugin-prettier": "^5.5.3",
|
|
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
63
|
"typescript": "^5.8.3",
|
|
64
|
-
"typescript-eslint": "^8.
|
|
64
|
+
"typescript-eslint": "^8.37.0",
|
|
65
65
|
"ws": "^8.18.3"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|