@lzwme/m3u8-dl 1.4.0 → 1.4.1

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.
@@ -1,2 +1,3 @@
1
+ import type { OutgoingHttpHeaders } from 'node:http';
1
2
  /** 从指定的 url 页面中提取 m3u8 播放地址。deep 指定搜索页面深度 */
2
- export declare function getM3u8Urls(url: string, deep?: number, visited?: Set<string>): Promise<Map<string, string>>;
3
+ export declare function getM3u8Urls(url: string, headers?: OutgoingHttpHeaders | string, deep?: number, visited?: Set<string>): Promise<Map<string, string>>;
@@ -4,20 +4,31 @@ exports.getM3u8Urls = getM3u8Urls;
4
4
  const fe_utils_1 = require("@lzwme/fe-utils");
5
5
  const utils_js_1 = require("./utils.js");
6
6
  /** 从指定的 url 页面中提取 m3u8 播放地址。deep 指定搜索页面深度 */
7
- async function getM3u8Urls(url, deep = 2, visited = new Set()) {
8
- const req = new fe_utils_1.ReqFetch({ headers: { 'content-type': 'text/html; charset=UTF-8' }, reqOptions: {} });
9
- const { data: html } = await req.get(url);
10
- // 从 html 中正则匹配提取 m3u8
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);
11
10
  const m3u8Urls = new Map();
11
+ if (!response.statusCode || response.statusCode >= 400) {
12
+ utils_js_1.logger.error('获取页面失败:', fe_utils_1.color.red(url), response.statusCode, response.statusMessage, html);
13
+ return m3u8Urls;
14
+ }
15
+ // 从 html 中正则匹配提取 m3u8
12
16
  const m3u8Regex = /https?:[^\s'"]+\.m3u8(\?[^\s'"]*)?/gi;
13
17
  // 1. 直接正则匹配 m3u8 地址
14
18
  let match = m3u8Regex.exec(html);
19
+ const title = (/<title>([^<]+)</.exec(html)?.[1].split('-')[0] || '').replace(/在线播放|详情|介绍|《|》/g, '').trim();
15
20
  while (match) {
16
- const title = (/<title>([^<]+)</.exec(html)?.[1].split('-')[0] || '').replace(/在线播放|《|》/g, '');
17
21
  const href = match[0].replaceAll('\\/', '/');
22
+ match = m3u8Regex.exec(html);
18
23
  if (!m3u8Urls.has(href))
19
24
  m3u8Urls.set(href, title);
20
- match = m3u8Regex.exec(html);
25
+ }
26
+ // 找到了多个链接,修改 title 添加序号
27
+ if (m3u8Urls.size > 3 && !/第.+(集|期)/.test(title)) {
28
+ let idx = 1;
29
+ for (const [key] of m3u8Urls) {
30
+ m3u8Urls.set(key, `${title}第${String(idx++).padStart(2, '0')}集`);
31
+ }
21
32
  }
22
33
  // 2. 若未找到且深度大于 0,则获取所有 a 标签的 href 并递归查找
23
34
  if (m3u8Urls.size === 0 && deep > 0) {
@@ -26,18 +37,33 @@ async function getM3u8Urls(url, deep = 2, visited = new Set()) {
26
37
  const aTagRegex = /<a\s+[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi;
27
38
  let aMatch = aTagRegex.exec(html);
28
39
  const origin = new URL(url).origin;
40
+ const subPageUrls = new Map();
41
+ let failedSubPages = 0;
29
42
  while (aMatch) {
30
43
  const href = aMatch[1] ? new URL(aMatch[1], origin).toString() : '';
31
- const text = aMatch[2];
44
+ const text = aMatch[2].replace(/<[^>]+>/g, '');
32
45
  aMatch = aTagRegex.exec(html);
33
- if (!href || visited.has(href) || !href.startsWith(origin) || !/集|HD|高清|播放/.test(text))
46
+ if (!href || visited.has(href) || !href.startsWith(origin))
34
47
  continue;
35
- visited.add(href);
48
+ if (!/集|期|HD|高清|抢先|BD/.test(text))
49
+ continue;
50
+ subPageUrls.set(href, text);
51
+ utils_js_1.logger.debug(' > 提取到m3u8链接: ', fe_utils_1.color.gray(href), text);
52
+ }
53
+ for (const [href, text] of subPageUrls) {
36
54
  try {
37
- const subUrls = await getM3u8Urls(href, deep - 1, visited);
55
+ visited.add(href);
56
+ const subUrls = await getM3u8Urls(href, headers, deep - 1, visited);
38
57
  utils_js_1.logger.debug(' > 从子页面提取: ', fe_utils_1.color.gray(href), text, subUrls.size);
58
+ if (subUrls.size === 0 && m3u8Urls.size === 0) {
59
+ failedSubPages++;
60
+ if (failedSubPages > 3) {
61
+ utils_js_1.logger.warn(`连续查找 ${failedSubPages} 个子页面均未获取到,不再继续`, url, href);
62
+ return m3u8Urls;
63
+ }
64
+ }
39
65
  for (const [u, t] of subUrls)
40
- m3u8Urls.set(u, t || text);
66
+ m3u8Urls.set(u, subUrls.size === 1 || /第.+(集|期)/.test(t) ? t : text);
41
67
  }
42
68
  catch (err) {
43
69
  utils_js_1.logger.warn(' > 尝试访问子页面异常: ', fe_utils_1.color.red(href), err.message);
@@ -1,8 +1,8 @@
1
- import type { IncomingHttpHeaders } from 'node:http';
1
+ import type { OutgoingHttpHeaders } from 'node:http';
2
2
  import type { M3u8Info } from '../types/m3u8';
3
3
  /**
4
4
  * 解析 m3u8 文件
5
5
  * @param content m3u8 文件的内容,可为 http 远程地址、本地文件路径
6
6
  * @param cacheDir 缓存文件保存目录
7
7
  */
8
- export declare function parseM3U8(content: string, cacheDir?: string, headers?: IncomingHttpHeaders): Promise<M3u8Info>;
8
+ export declare function parseM3U8(content: string, cacheDir?: string, headers?: OutgoingHttpHeaders | string): Promise<M3u8Info>;
@@ -15,7 +15,7 @@ async function parseM3U8(content, cacheDir = './cache', headers) {
15
15
  let url = process.cwd();
16
16
  if (content.startsWith('http')) {
17
17
  url = content;
18
- content = (await (0, utils_1.getRetry)(url)).data;
18
+ content = (await (0, utils_1.getRetry)(url, headers)).data;
19
19
  }
20
20
  else if (!content.includes('\n') && (0, node_fs_1.existsSync)(content)) {
21
21
  url = (0, node_path_1.resolve)(process.cwd(), content);
@@ -1,3 +1,3 @@
1
- import type { IncomingHttpHeaders } from 'node:http';
1
+ import type { OutgoingHttpHeaders } from 'node:http';
2
2
  import type { M3u8Crypto, TsItemInfo } from '../types/m3u8';
3
- export declare function tsDownload(info: TsItemInfo, cryptoInfo: M3u8Crypto, headers?: IncomingHttpHeaders): Promise<boolean>;
3
+ export declare function tsDownload(info: TsItemInfo, cryptoInfo: M3u8Crypto, headers?: OutgoingHttpHeaders | string): Promise<boolean>;
@@ -43,10 +43,7 @@ if (!node_worker_threads_1.isMainThread && node_worker_threads_1.parentPort) {
43
43
  const startTime = Date.now();
44
44
  if (data.options.debug)
45
45
  utils_js_1.logger.updateOptions({ levelType: 'debug' });
46
- let headers = data.options?.headers;
47
- if (headers)
48
- headers = (0, utils_js_1.formatHeaders)(headers);
49
- tsDownload(data.info, data.crypto, headers).then(success => {
46
+ tsDownload(data.info, data.crypto, data.options?.headers).then(success => {
50
47
  node_worker_threads_1.parentPort.postMessage({ success, info: data.info, timeCost: Date.now() - startTime });
51
48
  });
52
49
  });
@@ -1,11 +1,11 @@
1
1
  import { type Stats } from 'node:fs';
2
- import type { IncomingHttpHeaders } from 'node:http';
2
+ import type { OutgoingHttpHeaders } from 'node:http';
3
3
  import { NLogger, Request } from '@lzwme/fe-utils';
4
4
  export declare const request: Request;
5
- export declare const getRetry: <T = string>(url: string, headers?: IncomingHttpHeaders, retries?: number) => Promise<{
5
+ export declare const getRetry: <T = string>(url: string, headers?: OutgoingHttpHeaders | string, retries?: number) => Promise<{
6
6
  data: T;
7
7
  buffer: Buffer;
8
- headers: IncomingHttpHeaders;
8
+ headers: import("http").IncomingHttpHeaders;
9
9
  response: import("http").IncomingMessage;
10
10
  }>;
11
11
  export declare const logger: NLogger;
@@ -17,4 +17,4 @@ export declare function getLocation(url: string, method?: string): Promise<strin
17
17
  * 将传入的 headers 转换为统一的小写键对象格式
18
18
  * 如果 headers 是字符串,会先将其解析为对象;如果 headers 为空,则返回空对象。
19
19
  */
20
- export declare function formatHeaders(headers: string | IncomingHttpHeaders): Record<string, string>;
20
+ export declare function formatHeaders(headers: string | OutgoingHttpHeaders): Record<string, string>;
package/cjs/lib/utils.js CHANGED
@@ -13,7 +13,7 @@ exports.request = new fe_utils_1.Request({
13
13
  reqOptions: { rejectUnauthorized: false },
14
14
  });
15
15
  // process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
16
- const getRetry = (url, headers, retries = 3) => (0, fe_utils_1.retry)(() => exports.request.get(url, null, headers, { rejectUnauthorized: false }), 1000, retries, r => {
16
+ const getRetry = (url, headers, retries = 3) => (0, fe_utils_1.retry)(() => exports.request.get(url, null, formatHeaders(headers), { rejectUnauthorized: false }), 1000, retries, r => {
17
17
  if (r.response.statusCode !== 200) {
18
18
  console.log();
19
19
  exports.logger.warn(`[retry][${url}][${r.response.statusCode}]`, r.response.statusMessage || r.data);
@@ -59,10 +59,9 @@ async function getLocation(url, method = 'HEAD') {
59
59
  * 如果 headers 是字符串,会先将其解析为对象;如果 headers 为空,则返回空对象。
60
60
  */
61
61
  function formatHeaders(headers) {
62
- if (typeof headers === 'string') {
63
- headers = Object.fromEntries(headers.split('\n').map(line => line.split(':').map(d => d.trim())));
64
- }
65
- else if (!headers)
62
+ if (!headers)
66
63
  return {};
64
+ if (typeof headers === 'string')
65
+ headers = Object.fromEntries(headers.split('\n').map(line => line.split(':').map(d => d.trim())));
67
66
  return (0, fe_utils_1.toLowcaseKeyObject)(headers);
68
67
  }
@@ -45,6 +45,7 @@ export declare class DLServer {
45
45
  saveConfig(config: M3u8DLOptions, configPath?: string): void;
46
46
  private createApp;
47
47
  private startDownload;
48
+ startNextPending(): void;
48
49
  private wsSend;
49
50
  private initRouters;
50
51
  }
@@ -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);
@@ -249,19 +249,34 @@ class DLServer {
249
249
  wss.on('listening', () => utils_js_1.logger.info(`WebSocket server listening on port ${(0, console_log_colors_1.green)(this.options.port)}`));
250
250
  return { app, wss };
251
251
  }
252
- startDownload(url, options) {
252
+ async startDownload(url, options) {
253
+ if (!url)
254
+ return utils_js_1.logger.error('[satartDownload]Invalid URL:', url);
255
+ if (url.endsWith('.html')) {
256
+ const item = Array.from(await (0, getM3u8Urls_js_1.getM3u8Urls)(url, options.headers))[0];
257
+ if (!item)
258
+ return utils_js_1.logger.error('[startDownload]不是有效(包含)M3U8的地址:', url);
259
+ url = item[0];
260
+ if (!options.filename)
261
+ options.filename = item[1];
262
+ }
253
263
  const { options: dlOptions } = (0, format_options_js_1.formatOptions)(url, { ...this.cfg.dlOptions, ...options, cacheDir: this.options.cacheDir });
254
264
  const cacheItem = this.dlCache.get(url) || { options, dlOptions, status: 'pending', url };
255
265
  utils_js_1.logger.debug('startDownload', url, dlOptions, cacheItem.status);
256
266
  if (cacheItem.status === 'resume')
257
- return cacheItem.options;
267
+ return;
258
268
  if (cacheItem.localVideo && !(0, node_fs_1.existsSync)(cacheItem.localVideo))
259
269
  delete cacheItem.localVideo;
270
+ if (cacheItem.endTime)
271
+ delete cacheItem.endTime;
260
272
  cacheItem.status = this.downloading >= this.cfg.webOptions.maxDownloads ? 'pending' : 'resume';
273
+ // pending 优先级靠后
274
+ if (cacheItem.status === 'pending' && this.dlCache.has(url))
275
+ this.dlCache.delete(url);
261
276
  this.dlCache.set(url, cacheItem);
262
277
  this.wsSend('progress', url);
263
278
  if (cacheItem.status === 'pending')
264
- return cacheItem.options;
279
+ return;
265
280
  let workPoll = cacheItem.workPoll;
266
281
  const opts = {
267
282
  ...dlOptions,
@@ -270,7 +285,9 @@ class DLServer {
270
285
  workPoll = wp;
271
286
  },
272
287
  onProgress: (_finished, _total, current, stats) => {
273
- const item = this.dlCache.get(url) || cacheItem;
288
+ const item = this.dlCache.get(url);
289
+ if (!item)
290
+ return false; // 已删除
274
291
  const status = item.status || 'resume';
275
292
  Object.assign(item, { ...stats, current, options: dlOptions, status, workPoll, url });
276
293
  this.dlCache.set(url, item);
@@ -294,12 +311,7 @@ class DLServer {
294
311
  this.dlCache.set(url, item);
295
312
  this.wsSend('progress', url);
296
313
  this.saveCache();
297
- // 找到一个 pending 的任务,开始下载
298
- const nextItem = this.dlCache.entries().find(([_url, d]) => d.status === 'pending');
299
- if (nextItem) {
300
- this.startDownload(nextItem[0], nextItem[1].options);
301
- this.wsSend('progress', nextItem[0]);
302
- }
314
+ this.startNextPending();
303
315
  };
304
316
  try {
305
317
  if (dlOptions.type === 'parser') {
@@ -317,7 +329,14 @@ class DLServer {
317
329
  afterDownload({ filepath: '', errmsg: error.message }, url);
318
330
  utils_js_1.logger.error('下载失败:', error);
319
331
  }
320
- return dlOptions;
332
+ }
333
+ startNextPending() {
334
+ // 找到一个 pending 的任务,开始下载
335
+ const nextItem = this.dlCache.entries().find(([_url, d]) => d.status === 'pending');
336
+ if (nextItem) {
337
+ this.startDownload(nextItem[0], nextItem[1].options);
338
+ this.wsSend('progress', nextItem[0]);
339
+ }
321
340
  }
322
341
  wsSend(type = 'progress', data) {
323
342
  if (type === 'tasks' && !data) {
@@ -422,8 +441,10 @@ class DLServer {
422
441
  list.push(tItem);
423
442
  }
424
443
  }
425
- if (list.length)
444
+ if (list.length) {
426
445
  this.wsSend('progress', list);
446
+ this.startNextPending();
447
+ }
427
448
  res.json({ message: `已暂停 ${list.length} 个下载任务`, code: 0, count: list.length });
428
449
  });
429
450
  // API to resume download
@@ -477,6 +498,7 @@ class DLServer {
477
498
  if (list.length) {
478
499
  this.wsSend('delete', list);
479
500
  this.saveCache();
501
+ this.startNextPending();
480
502
  }
481
503
  res.json({ message: `已删除 ${list.length} 个下载任务`, code: 0, count: list.length });
482
504
  });
@@ -518,20 +540,36 @@ class DLServer {
518
540
  'Content-Length': String(stats.size),
519
541
  'Content-Type': ext === 'ts' ? 'video/mp2t' : ext === 'm3u8' ? 'application/vnd.apple.mpegurl' : ext === 'mp4' ? 'video/mp4' : 'text/plain',
520
542
  });
521
- res.setHeaders(headers).sendFile(filepath);
543
+ res.setHeaders(headers);
544
+ 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);
555
+ utils_js_1.logger.debug('[Localplay]file sent:', (0, console_log_colors_1.gray)(filepath), 'Size:', stats.size, 'bytes');
556
+ }
557
+ else {
558
+ res.sendFile(filepath);
559
+ }
522
560
  return;
523
561
  }
524
562
  }
525
- utils_js_1.logger.error('Localplay file not found:', filepath);
563
+ utils_js_1.logger.error('[Localplay]file not found:', (0, console_log_colors_1.red)(filepath));
526
564
  res.status(404).send({ message: 'Not Found', code: 404 });
527
565
  });
528
566
  app.post('/api/getM3u8Urls', (req, res) => {
529
- const url = req.body.url;
567
+ const { url, headers } = req.body;
530
568
  if (!url) {
531
569
  res.json({ code: 1001, message: '无效的 url 参数' });
532
570
  }
533
571
  else {
534
- (0, getM3u8Urls_js_1.getM3u8Urls)(url)
572
+ (0, getM3u8Urls_js_1.getM3u8Urls)(url, headers)
535
573
  .then(d => res.json({ code: 0, data: Array.from(d) }))
536
574
  .catch(err => res.json({ code: 401, message: err.message }));
537
575
  }
@@ -73,7 +73,7 @@ class VideoParser {
73
73
  }
74
74
  catch (error) {
75
75
  console.error('解析 URL 失败', url, error);
76
- return null;
76
+ return { url, platform: 'unknown' };
77
77
  }
78
78
  }
79
79
  /**
package/client/index.html CHANGED
@@ -170,9 +170,9 @@
170
170
  <input type="checkbox" :checked="selectedTasks.includes(task.url)"
171
171
  @change="toggleTaskSelection(task.url)"
172
172
  class="form-checkbox h-5 w-5 text-blue-500 rounded focus:ring-blue-500 mr-2"
173
- :title="'选择任务:' + (task.localVideo || task.filename || task.url)">
173
+ :title="'选择任务:' + task.showName">
174
174
  <h3 class="font-bold text-green-600 truncate max-w-[calc(100vw-100px)]" :title="task.url">
175
- {{ task.filename || task.localVideo || task.url }}
175
+ {{ task.showName }}
176
176
  </h3>
177
177
  <div class="absolute right-1 top-1 text-xs rounded overflow-hidden">
178
178
  <span v-if="task.status === 'pending'"
@@ -662,7 +662,7 @@ services:
662
662
  if (this.searchQuery) {
663
663
  const query = this.searchQuery.toLowerCase();
664
664
  tasks = tasks.filter(task => {
665
- const filename = (task.localVideo || task.filename || task.url).toLowerCase();
665
+ const filename = (task.localVideo || task.filename || task.dlOptions?.filename || task.url).toLowerCase();
666
666
  return filename.includes(query) || task.url.toLowerCase().includes(query);
667
667
  });
668
668
  }
@@ -674,7 +674,7 @@ services:
674
674
 
675
675
  // 排序:resume > pending > pause > error > done
676
676
  const statusOrder = { resume: 0, pending: 1, pause: 2, error: 3, done: 4 };
677
- tasks.sort((a, b) => (statusOrder[a.status] - statusOrder[b.status]) || (a.status === 'done' ? (b.filename - a.filename) : (b.endTime - a.endTime)));
677
+ tasks.sort((a, b) => (statusOrder[a.status] - statusOrder[b.status]) || (a.status === 'done' ? (b.filename - a.filename) : ((a.startTime || 0) - (b.startTime || 0))));
678
678
 
679
679
  // 更新 queueStatus
680
680
  const queueStatus = {
@@ -683,6 +683,7 @@ services:
683
683
  maxConcurrent: this.config.maxDownloads,
684
684
  };
685
685
  tasks.forEach(task => {
686
+ task.showName = task.filename || task.dlOptions?.filename || task.localVideo || task.url;
686
687
  if (task.status === 'pending') {
687
688
  queueStatus.queueLength++;
688
689
  } else if (task.status === 'resume') {
@@ -824,14 +825,14 @@ services:
824
825
  }
825
826
  },
826
827
  /** 显示新建下载弹窗 */
827
- showNewDownloadDialog: function () {
828
+ showNewDownloadDialog() {
828
829
  Swal.fire({
829
830
  title: '新建下载',
830
- width: '800px',
831
+ width: '900px',
831
832
  html: `
832
833
  <div class="text-left">
833
834
  <div class="flex flex-row gap-4">
834
- <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="">
835
836
  <div class="flex flex-row gap-1">
836
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">
837
838
  提取
@@ -877,8 +878,8 @@ services:
877
878
  preConfirm: () => {
878
879
  const urlsText = document.getElementById('downloadUrls').value.trim();
879
880
  const filename = document.getElementById('filename').value.trim();
880
- const saveDir = document.getElementById('saveDir').value.trim();
881
- const headersText = document.getElementById('headers').value.trim();
881
+ let saveDir = document.getElementById('saveDir').value.trim();
882
+ const headers = document.getElementById('headers').value.trim();
882
883
  const ignoreSegments = document.getElementById('ignoreSegments').value.trim();
883
884
 
884
885
  if (!urlsText) {
@@ -899,9 +900,14 @@ services:
899
900
  return false;
900
901
  }
901
902
 
903
+ if (urls.length > 1 && filename && !saveDir.includes(filename)) {
904
+ if (!saveDir) saveDir = this.config.saveDir;
905
+ saveDir = saveDir.replace(/\/?$/, '') + '/' + filename;
906
+ }
907
+
902
908
  return urls.map((item, idx) => ({
903
909
  url: item.url,
904
- filename: item.name || (filename ? `${filename}第${idx + 1}集` : ''),
910
+ filename: item.name || (filename ? `${filename}${urls.length > 1 ? `第${idx + 1}集` : ''}` : ''),
905
911
  saveDir,
906
912
  headers,
907
913
  ignoreSegments,
@@ -924,7 +930,7 @@ services:
924
930
  btn.setAttribute('disabled', 'disabled');
925
931
  btn.innerText = '解析中...';
926
932
 
927
- T.post('/api/getM3u8Urls', { url }).then(r => {
933
+ T.post('/api/getM3u8Urls', { url, headers: document.getElementById('headers').value.trim() }).then(r => {
928
934
  if (Array.isArray(r.data)) {
929
935
  document.getElementById('downloadUrls').value = r.data.map(d => d.join(' | ')).join('\n');
930
936
  T.toast(r.message || `解析完成!获取到 ${r.data.length} 个地址`);
@@ -941,7 +947,7 @@ services:
941
947
  try {
942
948
  list.forEach(async (item, idx) => {
943
949
  Object.entries(item).forEach(([key, value]) => !value && delete item[key]);
944
- this.tasks[item.url] = { status: 'resume', progress: 0, speed: 0, remainingTime: 0, size: 0 };
950
+ if (!/\.html?$/.test(item.url)) this.tasks[item.url] = { status: 'resume', progress: 0, speed: 0, remainingTime: 0, size: 0 };
945
951
  });
946
952
  const r = await T.post('/api/download', { list });
947
953
  if (!r.code) T.toast(r.message || '批量下载已开始');
@@ -1026,19 +1032,20 @@ services:
1026
1032
  },
1027
1033
  showTaskDetail(task) {
1028
1034
  console.log(task);
1035
+ const isResume = task.status === 'resume';
1029
1036
  const taskInfo = {
1030
1037
  名称: task.filename || task.localVideo,
1031
1038
  状态: T.taskStatus[task.status] || task.status,
1032
- 大小: `${T.formatSize(task.downloadedSize)} / ${task.size ? T.formatSize(task.size) : ''}`,
1033
- 分片: `<span class=text-green-600>${task.tsSuccess}</span> / <span class=text-red-500>${task.tsFailed}</span> / ${task.tsCount}`,
1034
- 进度: `${task.progress}%`,
1035
- 平均速度: `${task.avgSpeedDesc}/s`,
1039
+ 大小: `${T.formatSize(task.downloadedSize || 0)} / ${task.size ? T.formatSize(task.size) : ''}`,
1040
+ 分片: task.tsCount ? `<span class=text-green-600>${task.tsSuccess}</span> / <span class=text-red-500>${task.tsFailed}</span> / ${task.tsCount}` : '-',
1041
+ 进度: `${task.progress || '-'}%`,
1042
+ 平均速度: `${task.avgSpeedDesc || '-'}/s`,
1036
1043
  并发线程: task.threadNum,
1037
1044
  下载地址: task.url,
1038
1045
  保存位置: task.localVideo || task.options?.saveDir,
1039
- 开始时间: new Date(task.startTime).toLocaleString(),
1040
- 结束时间: task.endTime && new Date(task.endTime).toLocaleString(),
1041
- 预估还需: !task.endTime && task.remainingTime && T.formatTimeCost(task.remainingTime),
1046
+ 开始时间: task.startTime && new Date(task.startTime).toLocaleString(),
1047
+ 结束时间: !isResume && task.endTime && new Date(task.endTime).toLocaleString(),
1048
+ 预估还需: isResume && task.remainingTime && T.formatTimeCost(task.remainingTime),
1042
1049
  相关信息: task.errmsg && `<span class=text-red-600>${task.errmsg}</span>`
1043
1050
  };
1044
1051
  T.alert({
@@ -1057,7 +1064,8 @@ services:
1057
1064
  },
1058
1065
  /** 边下边播 */
1059
1066
  localPlay: function (task) {
1060
- const url = location.origin + '/localplay/' + (task.localVideo || task.localM3u8);
1067
+ const filepath = task.localVideo || task.localM3u8;
1068
+ const url = location.origin + `/localplay/${encodeURIComponent(filepath)}`;
1061
1069
  console.log(task);
1062
1070
  Swal.fire({
1063
1071
  title: task?.options.filename || task.url,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzwme/m3u8-dl",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
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,16 +44,16 @@
44
44
  "registry": "https://registry.npmjs.com"
45
45
  },
46
46
  "devDependencies": {
47
- "@biomejs/biome": "^2.0.6",
48
- "@eslint/js": "^9.29.0",
47
+ "@biomejs/biome": "^2.1.1",
48
+ "@eslint/js": "^9.30.1",
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.4",
52
+ "@types/node": "^24.0.12",
53
53
  "@types/ws": "^8.18.1",
54
- "@typescript-eslint/eslint-plugin": "^8.35.0",
55
- "@typescript-eslint/parser": "^8.35.0",
56
- "eslint": "^9.29.0",
54
+ "@typescript-eslint/eslint-plugin": "^8.36.0",
55
+ "@typescript-eslint/parser": "^8.36.0",
56
+ "eslint": "^9.30.1",
57
57
  "eslint-config-prettier": "^10.1.5",
58
58
  "eslint-plugin-prettier": "^5.5.1",
59
59
  "express": "^5.1.0",
@@ -61,8 +61,8 @@
61
61
  "prettier": "^3.6.2",
62
62
  "standard-version": "^9.5.0",
63
63
  "typescript": "^5.8.3",
64
- "typescript-eslint": "^8.35.0",
65
- "ws": "^8.18.2"
64
+ "typescript-eslint": "^8.36.0",
65
+ "ws": "^8.18.3"
66
66
  },
67
67
  "dependencies": {
68
68
  "@lzwme/fe-utils": "^1.9.0",