@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.
@@ -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];
@@ -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
- let content = (0, node_fs_1.readFileSync)(filepath);
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(`ws://${location.host}/ws?token=${this.token}`);
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 filepath = task.localVideo || task.localM3u8;
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?.options.filename || task.url,
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.1",
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.1.1",
48
- "@eslint/js": "^9.30.1",
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.2",
52
- "@types/node": "^24.0.12",
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.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.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.8.3",
64
- "typescript-eslint": "^8.36.0",
63
+ "typescript": "^5.9.2",
64
+ "typescript-eslint": "^8.41.0",
65
65
  "ws": "^8.18.3"
66
66
  },
67
67
  "dependencies": {