@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.
@@ -39,7 +39,7 @@ async function fileDownload(u, opts) {
39
39
  force: options.force,
40
40
  requestOptions: {
41
41
  headers: {
42
- referer: url,
42
+ referer: new URL(url).origin,
43
43
  ...(0, utils_js_1.formatHeaders)(options.headers),
44
44
  },
45
45
  rejectUnauthorized: false,
@@ -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(url: string, headers?: OutgoingHttpHeaders | string, deep?: number, visited?: Set<string>): Promise<Map<string, string>>;
11
+ export declare function getM3u8Urls(opts: GetM3u8UrlsOption): Promise<Map<string, string>>;
@@ -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(url, headers = {}, deep = 2, visited = new Set()) {
8
- const req = new fe_utils_1.Request({ headers: { 'content-type': 'text/html; charset=UTF-8', referer: new URL(url).origin, ...(0, utils_js_1.formatHeaders)(headers) } });
9
- const { data: html, response } = await req.get(url);
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(/在线播放|详情|介绍|《|》/g, '').trim();
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 && !/第.+(集|期)/.test(title)) {
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++).padStart(2, '0')}集`);
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], origin).toString() : '';
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(origin))
60
+ if (!href || options.visited.has(href) || !href.startsWith(baseUrl))
47
61
  continue;
48
- if (!/集|期|HD|高清|抢先|BD/.test(text))
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(' > 提取到m3u8链接: ', fe_utils_1.color.gray(href), text);
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(href, headers, deep - 1, visited);
57
- utils_js_1.logger.debug(' > 从子页面提取: ', fe_utils_1.color.gray(href), text, subUrls.size);
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
- m3u8Urls.set(u, subUrls.size === 1 || /第.+(集|期)/.test(t) ? t : text);
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: !['0', 'false'].includes(process.env.DS_LIMTE_FILE_ACCESS),
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=".+"\n?/g, '')
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="[实验性]输入视频播放页地址,尝试提取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="">
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
- T.post('/api/getM3u8Urls', { url, headers: document.getElementById('headers').value.trim() }).then(r => {
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.1",
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.1",
48
- "@eslint/js": "^9.30.1",
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.12",
52
+ "@types/node": "^24.0.15",
53
53
  "@types/ws": "^8.18.1",
54
- "@typescript-eslint/eslint-plugin": "^8.36.0",
55
- "@typescript-eslint/parser": "^8.36.0",
56
- "eslint": "^9.30.1",
57
- "eslint-config-prettier": "^10.1.5",
58
- "eslint-plugin-prettier": "^5.5.1",
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.36.0",
64
+ "typescript-eslint": "^8.37.0",
65
65
  "ws": "^8.18.3"
66
66
  },
67
67
  "dependencies": {