@lzwme/m3u8-dl 1.3.1 → 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.
package/cjs/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './lib/m3u8-download';
2
2
  export * from './lib/file-download';
3
+ export * from './lib/getM3u8Urls';
3
4
  export * from './lib/parseM3u8';
4
5
  export * from './video-parser';
package/cjs/index.js CHANGED
@@ -16,5 +16,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./lib/m3u8-download"), exports);
18
18
  __exportStar(require("./lib/file-download"), exports);
19
+ __exportStar(require("./lib/getM3u8Urls"), exports);
19
20
  __exportStar(require("./lib/parseM3u8"), exports);
20
21
  __exportStar(require("./video-parser"), exports);
@@ -8,7 +8,7 @@ const format_options_js_1 = require("./format-options.js");
8
8
  const utils_js_1 = require("./utils.js");
9
9
  async function fileDownload(u, opts) {
10
10
  utils_js_1.logger.debug('fileDownload', u, opts);
11
- const [url, options] = (0, format_options_js_1.formatOptions)(u, opts);
11
+ const { url, options } = (0, format_options_js_1.formatOptions)(u, opts);
12
12
  const startTime = Date.now();
13
13
  const stats = {
14
14
  url,
@@ -1,2 +1,6 @@
1
1
  import type { M3u8DLOptions } from '../types';
2
- export declare function formatOptions(url: string, opts: M3u8DLOptions): readonly [string, M3u8DLOptions, string];
2
+ export declare function formatOptions(url: string, opts: M3u8DLOptions): {
3
+ url: string;
4
+ options: M3u8DLOptions;
5
+ urlMd5: string;
6
+ };
@@ -76,5 +76,5 @@ function formatOptions(url, opts) {
76
76
  utils_1.logger.updateOptions({ levelType: 'debug' });
77
77
  utils_1.logger.debug('[m3u8-DL]options', options, url);
78
78
  }
79
- return [url, options, urlMd5];
79
+ return { url, options, urlMd5 };
80
80
  }
@@ -0,0 +1,3 @@
1
+ import type { OutgoingHttpHeaders } from 'node:http';
2
+ /** 从指定的 url 页面中提取 m3u8 播放地址。deep 指定搜索页面深度 */
3
+ export declare function getM3u8Urls(url: string, headers?: OutgoingHttpHeaders | string, deep?: number, visited?: Set<string>): Promise<Map<string, string>>;
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getM3u8Urls = getM3u8Urls;
4
+ const fe_utils_1 = require("@lzwme/fe-utils");
5
+ const utils_js_1 = require("./utils.js");
6
+ /** 从指定的 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);
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
16
+ const m3u8Regex = /https?:[^\s'"]+\.m3u8(\?[^\s'"]*)?/gi;
17
+ // 1. 直接正则匹配 m3u8 地址
18
+ let match = m3u8Regex.exec(html);
19
+ const title = (/<title>([^<]+)</.exec(html)?.[1].split('-')[0] || '').replace(/在线播放|详情|介绍|《|》/g, '').trim();
20
+ while (match) {
21
+ const href = match[0].replaceAll('\\/', '/');
22
+ match = m3u8Regex.exec(html);
23
+ if (!m3u8Urls.has(href))
24
+ m3u8Urls.set(href, title);
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
+ }
32
+ }
33
+ // 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);
37
+ const aTagRegex = /<a\s+[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi;
38
+ let aMatch = aTagRegex.exec(html);
39
+ const origin = new URL(url).origin;
40
+ const subPageUrls = new Map();
41
+ let failedSubPages = 0;
42
+ while (aMatch) {
43
+ const href = aMatch[1] ? new URL(aMatch[1], origin).toString() : '';
44
+ const text = aMatch[2].replace(/<[^>]+>/g, '');
45
+ aMatch = aTagRegex.exec(html);
46
+ if (!href || visited.has(href) || !href.startsWith(origin))
47
+ continue;
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) {
54
+ 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);
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
+ }
65
+ for (const [u, t] of subUrls)
66
+ m3u8Urls.set(u, subUrls.size === 1 || /第.+(集|期)/.test(t) ? t : text);
67
+ }
68
+ catch (err) {
69
+ utils_js_1.logger.warn(' > 尝试访问子页面异常: ', fe_utils_1.color.red(href), err.message);
70
+ }
71
+ }
72
+ }
73
+ return m3u8Urls;
74
+ }
75
+ // logger.updateOptions({ levelType: 'debug' });
76
+ // getM3u8Urls(process.argv.slice(2)[0]).then(d => console.log(d));
@@ -86,9 +86,8 @@ const cache = {
86
86
  };
87
87
  const tsDlFile = (0, node_path_1.resolve)(__dirname, './ts-download.js');
88
88
  exports.workPollPublic = new worker_pool_js_1.WorkerPool(tsDlFile);
89
- async function m3u8InfoParse(url, options = {}) {
90
- let urlMd5 = '';
91
- [url, options, urlMd5] = (0, format_options_js_1.formatOptions)(url, options);
89
+ async function m3u8InfoParse(u, o = {}) {
90
+ const { url, options, urlMd5 } = (0, format_options_js_1.formatOptions)(u, o);
92
91
  const ext = (0, utils_js_1.isSupportFfmpeg)() ? '.mp4' : '.ts';
93
92
  /** 最终合并转换后的文件路径 */
94
93
  let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename);
@@ -194,16 +193,12 @@ async function m3u8Download(url, options = {}) {
194
193
  return result;
195
194
  }
196
195
  if (result.m3u8Info?.tsCount > 0) {
197
- const workPoll = new worker_pool_js_1.WorkerPool(tsDlFile);
198
- let n = options.threadNum - workPoll.numThreads;
199
- if (n > 0)
200
- while (n--)
201
- workPoll.addNewWorker();
196
+ const workPoll = new worker_pool_js_1.WorkerPool(tsDlFile, options.threadNum);
202
197
  const { m3u8Info } = result;
203
198
  const startTime = Date.now();
204
199
  const barrier = new fe_utils_1.Barrier();
205
200
  /** 本地开始播放最少需要下载的 ts 文件数量 */
206
- const playStart = Math.min(options.threadNum + 2, result.m3u8Info.tsCount);
201
+ const playStart = Math.min(options.threadNum + 2, m3u8Info.tsCount);
207
202
  const stats = {
208
203
  url,
209
204
  startTime,
@@ -301,6 +296,7 @@ async function m3u8Download(url, options = {}) {
301
296
  options.onInited(stats, m3u8Info, workPoll);
302
297
  runTask(m3u8Info.data);
303
298
  await barrier.wait();
299
+ workPoll.close();
304
300
  if (stats.tsFailed > 0) {
305
301
  utils_js_1.logger.warn('Download Failed! Please retry!', stats.tsFailed);
306
302
  }
@@ -319,5 +315,9 @@ async function m3u8Download(url, options = {}) {
319
315
  function m3u8DLStop(url, wp = exports.workPollPublic) {
320
316
  if (!wp?.removeTask)
321
317
  return 0;
322
- return wp.removeTask(task => task.url === url);
318
+ const count = wp.removeTask(task => task.url === url);
319
+ // 进行中的任务,最多允许继续下载 10s
320
+ if (count === 0 && wp !== exports.workPollPublic)
321
+ setTimeout(() => wp.close(), 10_000);
322
+ return count;
323
323
  }
@@ -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
  }
@@ -87,6 +87,10 @@ class WorkerPool extends node_events_1.EventEmitter {
87
87
  this.numThreads = this.workers.length;
88
88
  }
89
89
  runTask(task, callback) {
90
+ if (this.totalNum === 0) {
91
+ console.error('未初始化 worker 或已销毁');
92
+ return;
93
+ }
90
94
  if (this.freeWorkers.length === 0) {
91
95
  this.tasks.push({ task, callback });
92
96
  return;
@@ -38,8 +38,8 @@ async function formatUrls(urls, options) {
38
38
  continue;
39
39
  }
40
40
  }
41
- const [u, o] = (0, format_options_1.formatOptions)(url, options);
42
- taskset.set(u, o);
41
+ const r = (0, format_options_1.formatOptions)(url, options);
42
+ taskset.set(r.url, r.options);
43
43
  }
44
44
  return taskset;
45
45
  }
@@ -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
  }
@@ -42,6 +42,7 @@ const console_log_colors_1 = require("console-log-colors");
42
42
  const file_download_js_1 = require("../lib/file-download.js");
43
43
  const format_options_js_1 = require("../lib/format-options.js");
44
44
  const m3u8_download_js_1 = require("../lib/m3u8-download.js");
45
+ const getM3u8Urls_js_1 = require("../lib/getM3u8Urls.js");
45
46
  const utils_js_1 = require("../lib/utils.js");
46
47
  const index_js_1 = require("../video-parser/index.js");
47
48
  const rootDir = (0, node_path_1.resolve)(__dirname, '../..');
@@ -128,7 +129,7 @@ class DLServer {
128
129
  return;
129
130
  }
130
131
  this.dlCache.forEach(item => {
131
- if (item.status === 'done' && item.localVideo && !(0, node_fs_1.existsSync)(item.localVideo)) {
132
+ if (item.status === 'done' && (!item.localVideo || !(0, node_fs_1.existsSync)(item.localVideo))) {
132
133
  item.status = 'error';
133
134
  item.errmsg = '已删除';
134
135
  }
@@ -197,7 +198,7 @@ class DLServer {
197
198
  if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(rootDir, 'client/local/cdn'))) {
198
199
  indexHtml = indexHtml
199
200
  .replaceAll('https://s4.zstatic.net/ajax/libs', 'local/cdn')
200
- .replaceAll(/integrity=.+\n/g, '')
201
+ .replaceAll(/integrity=".+"\n?/g, '')
201
202
  .replace('https://cdn.tailwindcss.com/3.4.16', 'local/cdn/tailwindcss/3.4.16/tailwindcss.min.js');
202
203
  }
203
204
  res.setHeader('content-type', 'text/html').send(indexHtml);
@@ -248,19 +249,34 @@ class DLServer {
248
249
  wss.on('listening', () => utils_js_1.logger.info(`WebSocket server listening on port ${(0, console_log_colors_1.green)(this.options.port)}`));
249
250
  return { app, wss };
250
251
  }
251
- startDownload(url, options) {
252
- const dlOptions = (0, format_options_js_1.formatOptions)(url, { ...this.cfg.dlOptions, ...options, cacheDir: this.options.cacheDir })[1];
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
+ }
263
+ const { options: dlOptions } = (0, format_options_js_1.formatOptions)(url, { ...this.cfg.dlOptions, ...options, cacheDir: this.options.cacheDir });
253
264
  const cacheItem = this.dlCache.get(url) || { options, dlOptions, status: 'pending', url };
254
265
  utils_js_1.logger.debug('startDownload', url, dlOptions, cacheItem.status);
255
266
  if (cacheItem.status === 'resume')
256
- return cacheItem.options;
267
+ return;
257
268
  if (cacheItem.localVideo && !(0, node_fs_1.existsSync)(cacheItem.localVideo))
258
269
  delete cacheItem.localVideo;
270
+ if (cacheItem.endTime)
271
+ delete cacheItem.endTime;
259
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);
260
276
  this.dlCache.set(url, cacheItem);
261
277
  this.wsSend('progress', url);
262
278
  if (cacheItem.status === 'pending')
263
- return cacheItem.options;
279
+ return;
264
280
  let workPoll = cacheItem.workPoll;
265
281
  const opts = {
266
282
  ...dlOptions,
@@ -269,7 +285,9 @@ class DLServer {
269
285
  workPoll = wp;
270
286
  },
271
287
  onProgress: (_finished, _total, current, stats) => {
272
- const item = this.dlCache.get(url) || cacheItem;
288
+ const item = this.dlCache.get(url);
289
+ if (!item)
290
+ return false; // 已删除
273
291
  const status = item.status || 'resume';
274
292
  Object.assign(item, { ...stats, current, options: dlOptions, status, workPoll, url });
275
293
  this.dlCache.set(url, item);
@@ -293,12 +311,7 @@ class DLServer {
293
311
  this.dlCache.set(url, item);
294
312
  this.wsSend('progress', url);
295
313
  this.saveCache();
296
- // 找到一个 pending 的任务,开始下载
297
- const nextItem = this.dlCache.entries().find(([_url, d]) => d.status === 'pending');
298
- if (nextItem) {
299
- this.startDownload(nextItem[0], nextItem[1].options);
300
- this.wsSend('progress', nextItem[0]);
301
- }
314
+ this.startNextPending();
302
315
  };
303
316
  try {
304
317
  if (dlOptions.type === 'parser') {
@@ -316,7 +329,14 @@ class DLServer {
316
329
  afterDownload({ filepath: '', errmsg: error.message }, url);
317
330
  utils_js_1.logger.error('下载失败:', error);
318
331
  }
319
- 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
+ }
320
340
  }
321
341
  wsSend(type = 'progress', data) {
322
342
  if (type === 'tasks' && !data) {
@@ -337,24 +357,23 @@ class DLServer {
337
357
  }
338
358
  initRouters() {
339
359
  const { app } = this;
340
- // health check
341
360
  app.get('/healthcheck', (_req, res) => {
342
361
  res.json({ message: 'ok', code: 0 });
343
362
  });
344
- app.post('/config', (req, res) => {
363
+ app.post('/api/config', (req, res) => {
345
364
  const config = req.body;
346
365
  this.saveConfig(config);
347
366
  res.json({ message: 'Config updated successfully', code: 0 });
348
367
  });
349
- app.get('/config', (_req, res) => {
368
+ app.get('/api/config', (_req, res) => {
350
369
  res.json({ ...this.cfg.dlOptions, ...this.cfg.webOptions });
351
370
  });
352
371
  // API to get all download progress
353
- app.get('/tasks', (_req, res) => {
372
+ app.get('/api/tasks', (_req, res) => {
354
373
  res.json(Object.fromEntries(this.dlCacheClone()));
355
374
  });
356
375
  // API to get queue status
357
- app.get('/queue/status', (_req, res) => {
376
+ app.get('/api/queue/status', (_req, res) => {
358
377
  const pendingTasks = Array.from(this.dlCache.entries()).filter(([_, item]) => item.status === 'pending');
359
378
  const activeTasks = Array.from(this.dlCache.entries()).filter(([_, item]) => item.status === 'resume');
360
379
  res.json({
@@ -364,7 +383,7 @@ class DLServer {
364
383
  });
365
384
  });
366
385
  // API to clear queue
367
- app.post('/queue/clear', (_req, res) => {
386
+ app.post('/api/queue/clear', (_req, res) => {
368
387
  let count = 0;
369
388
  for (const [url, item] of this.dlCache.entries()) {
370
389
  if (item.status === 'pending') {
@@ -377,7 +396,7 @@ class DLServer {
377
396
  res.json({ message: `已清空 ${count} 个等待中的下载任务`, code: 0 });
378
397
  });
379
398
  // API to update task priority
380
- app.post('/priority', (req, res) => {
399
+ app.post('/api/priority', (req, res) => {
381
400
  const { url, priority } = req.body;
382
401
  const item = this.dlCache.get(url);
383
402
  if (!item) {
@@ -389,7 +408,7 @@ class DLServer {
389
408
  res.json({ message: '已更新任务优先级', code: 0 });
390
409
  });
391
410
  // API to start m3u8 download
392
- app.post('/download', (req, res) => {
411
+ app.post('/api/download', (req, res) => {
393
412
  const { url, options = {}, list = [] } = req.body;
394
413
  try {
395
414
  if (list.length) {
@@ -409,7 +428,7 @@ class DLServer {
409
428
  }
410
429
  });
411
430
  // API to pause download
412
- app.post('/pause', (req, res) => {
431
+ app.post('/api/pause', (req, res) => {
413
432
  const { urls, all = false } = req.body;
414
433
  const urlsToPause = all ? [...this.dlCache.keys()] : urls;
415
434
  const list = [];
@@ -417,17 +436,19 @@ class DLServer {
417
436
  const item = this.dlCache.get(url);
418
437
  if (['resume', 'pending'].includes(item?.status)) {
419
438
  (0, m3u8_download_js_1.m3u8DLStop)(url, item.workPoll);
420
- item.status = item.tsSuccess === item.tsCount ? 'done' : 'pause';
439
+ item.status = item.tsSuccess > 0 && item.tsSuccess === item.tsCount ? 'done' : 'pause';
421
440
  const { workPoll, ...tItem } = item;
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
430
- app.post('/resume', (req, res) => {
451
+ app.post('/api/resume', (req, res) => {
431
452
  const { urls, all = false } = req.body;
432
453
  const urlsToResume = all ? [...this.dlCache.keys()] : urls;
433
454
  const list = [];
@@ -446,7 +467,7 @@ class DLServer {
446
467
  res.json({ message: list.length ? `已恢复 ${list.length} 个下载任务` : '没有找到可恢复的下载任务', code: 0, count: list.length });
447
468
  });
448
469
  // API to delete download
449
- app.post('/delete', (req, res) => {
470
+ app.post('/api/delete', (req, res) => {
450
471
  const { urls, deleteCache = false, deleteVideo = false } = req.body;
451
472
  const urlsToDelete = urls;
452
473
  const list = [];
@@ -458,20 +479,27 @@ class DLServer {
458
479
  list.push(item.url);
459
480
  if (deleteCache && item.current?.tsOut) {
460
481
  const cacheDir = (0, node_path_1.dirname)(item.current.tsOut);
461
- if ((0, node_fs_1.existsSync)(cacheDir))
482
+ if ((0, node_fs_1.existsSync)(cacheDir)) {
462
483
  (0, node_fs_1.rmSync)(cacheDir, { recursive: true });
484
+ utils_js_1.logger.debug('删除缓存目录:', cacheDir);
485
+ }
463
486
  }
464
487
  if (deleteVideo) {
465
488
  ['.ts', '.mp4'].forEach(ext => {
466
489
  const filepath = (0, node_path_1.resolve)(item.options.saveDir, item.options.filename + ext);
467
- if ((0, node_fs_1.existsSync)(filepath))
490
+ if ((0, node_fs_1.existsSync)(filepath)) {
468
491
  (0, node_fs_1.unlinkSync)(filepath);
492
+ utils_js_1.logger.debug('删除文件:', filepath);
493
+ }
469
494
  });
470
495
  }
471
496
  }
472
497
  }
473
- if (list.length)
498
+ if (list.length) {
474
499
  this.wsSend('delete', list);
500
+ this.saveCache();
501
+ this.startNextPending();
502
+ }
475
503
  res.json({ message: `已删除 ${list.length} 个下载任务`, code: 0, count: list.length });
476
504
  });
477
505
  app.get(/^\/localplay\/(.*)$/, (req, res) => {
@@ -512,13 +540,40 @@ class DLServer {
512
540
  'Content-Length': String(stats.size),
513
541
  'Content-Type': ext === 'ts' ? 'video/mp2t' : ext === 'm3u8' ? 'application/vnd.apple.mpegurl' : ext === 'mp4' ? 'video/mp4' : 'text/plain',
514
542
  });
515
- 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
+ }
516
560
  return;
517
561
  }
518
562
  }
519
- 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));
520
564
  res.status(404).send({ message: 'Not Found', code: 404 });
521
565
  });
566
+ app.post('/api/getM3u8Urls', (req, res) => {
567
+ const { url, headers } = req.body;
568
+ if (!url) {
569
+ res.json({ code: 1001, message: '无效的 url 参数' });
570
+ }
571
+ else {
572
+ (0, getM3u8Urls_js_1.getM3u8Urls)(url, headers)
573
+ .then(d => res.json({ code: 0, data: Array.from(d) }))
574
+ .catch(err => res.json({ code: 401, message: err.message }));
575
+ }
576
+ });
522
577
  }
523
578
  }
524
579
  exports.DLServer = DLServer;
@@ -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
@@ -7,7 +7,7 @@
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
8
  <meta name="apple-mobile-web-app-capable" content="yes">
9
9
  <title>M3U8 下载器</title>
10
- <link rel="icon" type="image/svg+xml" href="logo.svg">
10
+ <link rel="icon" type="image/png" href="logo.png">
11
11
  <link rel="stylesheet" href="https://s4.zstatic.net/ajax/libs/font-awesome/6.7.2/css/all.min.css"
12
12
  integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg=="
13
13
  crossorigin="anonymous" referrerpolicy="no-referrer" />
@@ -36,7 +36,7 @@
36
36
  <div class="sidebar p-4" :class="{ 'show': !sidebarCollapsed }">
37
37
  <div class="mb-8">
38
38
  <div class="flex items-center mb-4">
39
- <img src="logo.svg" alt="M3U8 下载器" class="w-8 h-8 mr-2">
39
+ <img src="logo.png" alt="M3U8 下载器" class="w-8 h-8 mr-2">
40
40
  <h1 class="text-xl font-bold text-gray-800">M3U8 下载器</h1>
41
41
  </div>
42
42
  <button @click="showNewDownloadDialog"
@@ -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'"
@@ -521,7 +521,7 @@ services:
521
521
  alert(msg, p) {
522
522
  p = typeof msg === 'object' ? msg : Object.assign({ text: msg }, p);
523
523
  if (!p.toast) p.allowOutsideClick = false;
524
- return Swal.fire(Object.assign({ icon: 'info', showConfirmButton: false, showCloseButton: true }, p));
524
+ return Swal.fire(Object.assign({ icon: 'info', showConfirmButton: false, showCloseButton: true, confirmButtonText: '确定', cancelButtonText: '关闭' }, p));
525
525
  },
526
526
  confirm(msg, p) {
527
527
  return this.alert(msg, { showConfirmButton: true, showCancelButton: true, showCloseButton: true, confirmButtonText: '确认', cancelButtonText: '取消' });
@@ -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') {
@@ -790,7 +791,7 @@ services:
790
791
  },
791
792
  /** 获取配置 */
792
793
  fetchConfig: async function () {
793
- const config = await T.get('/config');
794
+ const config = await T.get('/api/config');
794
795
  if (config.code) {
795
796
  console.error('获取配置失败:', config);
796
797
  T.alert('获取配置失败: ' + config.message, { icon: 'error' });
@@ -803,7 +804,7 @@ services:
803
804
  },
804
805
  /** 更新配置 */
805
806
  updateConfig: async function () {
806
- const result = await T.post('/config', this.config);
807
+ const result = await T.post('/api/config', this.config);
807
808
  T.toast(result.message || '配置已更新', { icon: result.code ? 'error' : 'success' });
808
809
  },
809
810
  updateLocalConfig: async function () {
@@ -824,24 +825,37 @@ 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
- <label class="block text-sm font-bold text-gray-700 mb-1">视频链接(每行一个,支持m3u8地址及抖音、微博、皮皮虾视频分享链接)</label>
834
- <textarea id="downloadUrls" class="w-full p-2 border rounded-lg focus:ring-blue-500" rows="3" placeholder="格式: URL | 名称(可选)、名称 | URL"></textarea>
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="">
836
+ <div class="flex flex-row gap-1">
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
+ 提取
839
+ </button>
840
+ </div>
841
+ </div>
835
842
 
836
843
  <div class="mt-4">
837
- <label class="block text-sm font-bold text-gray-700 mb-1">视频名称(可选)</label>
838
- <input id="filename" class="w-full p-2 border rounded-lg focus:border-blue-500" placeholder="请输入视频名称">
839
- <p class="mt-1 text-sm text-gray-500">若输入多个链接,将依次以"视频名称+第N集"命名</p>
844
+ <label class="block text-sm font-bold text-gray-700 mb-1">视频链接(每行一个,支持m3u8地址及抖音、微博、皮皮虾视频分享链接)</label>
845
+ <textarea id="downloadUrls" class="w-full p-2 border rounded-lg focus:ring-blue-500" rows="3" placeholder="格式: URL | 名称(可选)、名称 | URL"></textarea>
840
846
  </div>
841
847
 
842
848
  <div class="mt-4">
849
+ <div class="flex flex-row gap-2 items-center">
850
+ <label class="block text-sm font-bold text-gray-700 mb-1">视频名称</label>
851
+ <input id="filename" class="flex-1 p-2 border rounded-lg focus:border-blue-500" placeholder="请输入视频名称(可选)">
852
+ </div>
853
+ <p class="ml-2 mt-1 text-sm text-gray-500">若输入多个链接,将依次以"视频名称+第N集"命名</p>
854
+ </div>
855
+
856
+ <div class="mt-4 flex flex-row gap-2 items-center">
843
857
  <label class="block text-sm font-bold text-gray-700 mb-1">保存位置</label>
844
- <input id="saveDir" class="w-full p-2 border rounded-lg focus:ring-blue-500" placeholder="请输入保存路径" value="${this.config.saveDir || ''}">
858
+ <input id="saveDir" class="flex-1 p-2 border rounded-lg focus:ring-blue-500" placeholder="请输入保存路径" value="${this.config.saveDir || ''}">
845
859
  </div>
846
860
 
847
861
  <div class="mt-4">
@@ -851,7 +865,7 @@ services:
851
865
 
852
866
  <div class="mt-4">
853
867
  <label class="block text-sm font-bold text-gray-700 mb-1">自定义请求头</label>
854
- <textarea id="headers" class="w-full p-2 border rounded-lg focus:ring-blue-500" rows="4" placeholder="每行一个请求头(微博视频必须设置 Cookie),格式:Key: Value&#10;例如:&#10;Referer: https://example.com&#10;Cookie: token=123"></textarea>
868
+ <textarea id="headers" class="w-full p-2 border rounded-lg focus:ring-blue-500" rows="3" placeholder="每行一个(微博视频须设置 Cookie),格式:Key: Value。例如:&#10;Referer: https://example.com&#10;Cookie: token=123"></textarea>
855
869
  </div>
856
870
  </div>
857
871
  `,
@@ -864,8 +878,8 @@ services:
864
878
  preConfirm: () => {
865
879
  const urlsText = document.getElementById('downloadUrls').value.trim();
866
880
  const filename = document.getElementById('filename').value.trim();
867
- const saveDir = document.getElementById('saveDir').value.trim();
868
- const headersText = document.getElementById('headers').value.trim();
881
+ let saveDir = document.getElementById('saveDir').value.trim();
882
+ const headers = document.getElementById('headers').value.trim();
869
883
  const ignoreSegments = document.getElementById('ignoreSegments').value.trim();
870
884
 
871
885
  if (!urlsText) {
@@ -886,9 +900,14 @@ services:
886
900
  return false;
887
901
  }
888
902
 
903
+ if (urls.length > 1 && filename && !saveDir.includes(filename)) {
904
+ if (!saveDir) saveDir = this.config.saveDir;
905
+ saveDir = saveDir.replace(/\/?$/, '') + '/' + filename;
906
+ }
907
+
889
908
  return urls.map((item, idx) => ({
890
909
  url: item.url,
891
- filename: item.name || (filename ? `${filename}第${idx + 1}集` : ''),
910
+ filename: item.name || (filename ? `${filename}${urls.length > 1 ? `第${idx + 1}集` : ''}` : ''),
892
911
  saveDir,
893
912
  headers,
894
913
  ignoreSegments,
@@ -897,15 +916,40 @@ services:
897
916
  }).then((result) => {
898
917
  if (result.isConfirmed) this.startBatchDownload(result.value);
899
918
  });
919
+
920
+ setTimeout(() => {
921
+ const btn = document.getElementById('getM3u8UrlsBtn');
922
+ if (!btn) return;
923
+
924
+ btn.addEventListener('click', async () => {
925
+ const url = document.getElementById('playUrl').value.trim();
926
+ if (!/^https?:/.test(url)) {
927
+ return Swal.showValidationMessage('请输入正确的 URL 地址');
928
+ }
929
+
930
+ btn.setAttribute('disabled', 'disabled');
931
+ btn.innerText = '解析中...';
932
+
933
+ T.post('/api/getM3u8Urls', { url, headers: document.getElementById('headers').value.trim() }).then(r => {
934
+ if (Array.isArray(r.data)) {
935
+ document.getElementById('downloadUrls').value = r.data.map(d => d.join(' | ')).join('\n');
936
+ T.toast(r.message || `解析完成!获取到 ${r.data.length} 个地址`);
937
+ }
938
+
939
+ btn.removeAttribute('disabled');
940
+ btn.innerText = '提取';
941
+ });
942
+ });
943
+ }, 500);
900
944
  },
901
945
  /** 批量下载 */
902
946
  startBatchDownload: async function (list) {
903
947
  try {
904
948
  list.forEach(async (item, idx) => {
905
949
  Object.entries(item).forEach(([key, value]) => !value && delete item[key]);
906
- 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 };
907
951
  });
908
- const r = await T.post('/download', { list });
952
+ const r = await T.post('/api/download', { list });
909
953
  if (!r.code) T.toast(r.message || '批量下载已开始');
910
954
  this.forceUpdate();
911
955
  } catch (error) {
@@ -917,7 +961,7 @@ services:
917
961
  pauseDownload: async function (urls) {
918
962
  if (!urls) urls = this.selectedTasks;
919
963
  if (typeof urls === 'string') urls = [urls];
920
- const r = await T.post(`/pause`, { urls, all: urls[0] === 'all' });
964
+ const r = await T.post('/api/pause', { urls, all: urls[0] === 'all' });
921
965
  if (!r.code) T.toast(r.message || '已暂停下载');
922
966
  if (urls === this.selectedTasks) this.selectedTasks = [];
923
967
  },
@@ -925,7 +969,7 @@ services:
925
969
  resumeDownload: async function (urls) {
926
970
  if (!urls) urls = this.selectedTasks;
927
971
  if (typeof urls === 'string') urls = [urls];
928
- const r = await T.post(`/resume`, { urls, all: urls[0] === 'all' });
972
+ const r = await T.post('/api/resume', { urls, all: urls[0] === 'all' });
929
973
  if (!r.code) T.toast(r.message || '已恢复下载');
930
974
  if (urls === this.selectedTasks) this.selectedTasks = [];
931
975
  },
@@ -965,7 +1009,7 @@ services:
965
1009
  });
966
1010
 
967
1011
  if (result.isConfirmed) {
968
- const r = await T.post(`/delete`, {
1012
+ const r = await T.post('/api/delete', {
969
1013
  urls,
970
1014
  deleteCache: result.value.deleteCache,
971
1015
  deleteVideo: result.value.deleteVideo
@@ -984,28 +1028,30 @@ services:
984
1028
  }
985
1029
  },
986
1030
  getTasks: async function () {
987
- this.tasks = await T.get('/tasks');
1031
+ this.tasks = await T.get('/api/tasks');
988
1032
  },
989
1033
  showTaskDetail(task) {
990
1034
  console.log(task);
1035
+ const isResume = task.status === 'resume';
991
1036
  const taskInfo = {
992
1037
  名称: task.filename || task.localVideo,
993
1038
  状态: T.taskStatus[task.status] || task.status,
994
- 大小: `${T.formatSize(task.downloadedSize)} / ${task.size ? T.formatSize(task.size) : ''}`,
995
- 分片: `<span class=text-green-600>${task.tsSuccess}</span> / <span class=text-red-500>${task.tsFailed}</span> / ${task.tsCount}`,
996
- 进度: `${task.progress}%`,
997
- 平均速度: `${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`,
998
1043
  并发线程: task.threadNum,
999
1044
  下载地址: task.url,
1000
1045
  保存位置: task.localVideo || task.options?.saveDir,
1001
- 开始时间: new Date(task.startTime).toLocaleString(),
1002
- 结束时间: task.endTime && new Date(task.endTime).toLocaleString(),
1003
- 预估还需: !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),
1004
1049
  相关信息: task.errmsg && `<span class=text-red-600>${task.errmsg}</span>`
1005
1050
  };
1006
- Swal.fire({
1051
+ T.alert({
1007
1052
  title: '任务详情',
1008
1053
  width: 1000,
1054
+ icon: '',
1009
1055
  html: [
1010
1056
  '<div class="flex-col full-width text-left">',
1011
1057
  Object.entries(taskInfo).filter(d => d[1]).map(
@@ -1018,7 +1064,8 @@ services:
1018
1064
  },
1019
1065
  /** 边下边播 */
1020
1066
  localPlay: function (task) {
1021
- const url = location.origin + '/localplay/' + (task.localVideo || task.localM3u8);
1067
+ const filepath = task.localVideo || task.localM3u8;
1068
+ const url = location.origin + `/localplay/${encodeURIComponent(filepath)}`;
1022
1069
  console.log(task);
1023
1070
  Swal.fire({
1024
1071
  title: task?.options.filename || task.url,
@@ -1054,12 +1101,12 @@ services:
1054
1101
  },
1055
1102
  // 更新任务优先级
1056
1103
  updatePriority: async function (url, priority) {
1057
- const r = await T.post('/priority', { url, priority: parseInt(priority) });
1104
+ const r = await T.post('/api/priority', { url, priority: parseInt(priority) });
1058
1105
  T.toast(r.message || '已更新优先级');
1059
1106
  },
1060
1107
  // 获取队列状态
1061
1108
  getQueueStatus: async function () {
1062
- const status = await T.get('/queue/status');
1109
+ const status = await T.get('/api/queue/status');
1063
1110
  if (status?.maxConcurrent) this.queueStatus = status;
1064
1111
  },
1065
1112
  // 清空下载队列
@@ -1075,7 +1122,7 @@ services:
1075
1122
  });
1076
1123
 
1077
1124
  if (result.isConfirmed) {
1078
- const r = await T.post('/queue/clear');
1125
+ const r = await T.post('/api/queue/clear');
1079
1126
  T.toast(r.message || '已清空下载队列');
1080
1127
  this.getQueueStatus();
1081
1128
  }
Binary file
package/client/style.css CHANGED
@@ -1,3 +1,11 @@
1
+ ::placeholder {
2
+ font-size: 14px;
3
+ }
4
+
5
+ .swal2-html-container textarea, input {
6
+ font-size: 16px !important;
7
+ }
8
+
1
9
  #app {
2
10
  max-width: 1400px;
3
11
  margin: auto;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzwme/m3u8-dl",
3
- "version": "1.3.1",
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,25 +44,25 @@
44
44
  "registry": "https://registry.npmjs.com"
45
45
  },
46
46
  "devDependencies": {
47
- "@biomejs/biome": "^2.0.0",
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.3",
52
+ "@types/node": "^24.0.12",
53
53
  "@types/ws": "^8.18.1",
54
- "@typescript-eslint/eslint-plugin": "^8.34.1",
55
- "@typescript-eslint/parser": "^8.34.1",
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
- "eslint-plugin-prettier": "^5.5.0",
58
+ "eslint-plugin-prettier": "^5.5.1",
59
59
  "express": "^5.1.0",
60
60
  "husky": "^9.1.7",
61
- "prettier": "^3.5.3",
61
+ "prettier": "^3.6.2",
62
62
  "standard-version": "^9.5.0",
63
63
  "typescript": "^5.8.3",
64
- "typescript-eslint": "^8.34.1",
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",
package/client/logo.svg DELETED
@@ -1,16 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
3
- <!-- 背景圆形 -->
4
- <circle cx="256" cy="256" r="240" fill="#1890ff" opacity="0.1"/>
5
-
6
- <!-- 下载箭头 -->
7
- <path d="M256 128v192M208 256l48 48 48-48" stroke="#1890ff" stroke-width="24" stroke-linecap="round" stroke-linejoin="round"/>
8
-
9
- <!-- M3U8 文字 -->
10
- <text x="256" y="384" font-family="Arial, sans-serif" font-size="64" font-weight="bold" fill="#1890ff" text-anchor="middle">M3U8</text>
11
-
12
- <!-- 速度线条 -->
13
- <path d="M128 192c32-32 64-32 96 0s64 32 96 0" stroke="#1890ff" stroke-width="16" stroke-linecap="round" opacity="0.6"/>
14
- <path d="M128 224c32-32 64-32 96 0s64 32 96 0" stroke="#1890ff" stroke-width="16" stroke-linecap="round" opacity="0.4"/>
15
- <path d="M128 256c32-32 64-32 96 0s64 32 96 0" stroke="#1890ff" stroke-width="16" stroke-linecap="round" opacity="0.2"/>
16
- </svg>