@lzwme/m3u8-dl 1.1.2 → 1.2.0

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/README.MD CHANGED
@@ -128,13 +128,13 @@ for (const filepath of fileList) {
128
128
 
129
129
  ```bash
130
130
  # docker pull ghcr.io/lzwme/m3u8-dl:latest
131
- docker pull lzwme/m3u8dl-dl:latest
131
+ docker pull renxia/m3u8dl-dl:latest
132
132
 
133
133
  docker run --rm -it \
134
134
  -v ./cache:/app/cache \
135
135
  -v ./downloads:/app/downloads \
136
136
  -p 6600:6600 \
137
- lzwme/m3u8dl-server:latest
137
+ renxia/m3u8dl-dl:latest
138
138
  ```
139
139
 
140
140
  也可以基于 [docker-compose.yml](./docker/docker-compose.yml) 部署:
package/cjs/cli.js CHANGED
@@ -66,6 +66,7 @@ commander_1.program
66
66
  .option('--no-del-cache', `下载成功后是否删除临时文件。默认为 true。保存临时文件可以在重复下载时识别缓存`)
67
67
  .option('--no-convert', '下载成功后,是否不合并转换为 mp4 文件。默认为 true。')
68
68
  .option('-H, --headers <headers>', `自定义请求头。格式为 key1=value1\nkey2=value2`)
69
+ .option('-T, --type <type>', `指定下载类型。默认根据URL自动识别,如果是批量下载多个不同 URL 类型,请不要设置。可选值:m3u8, file, parser`)
69
70
  .action(async (urls) => {
70
71
  const options = getOptions();
71
72
  utils_js_1.logger.debug(urls, options);
package/cjs/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export * from './lib/m3u8-download';
2
+ export * from './lib/file-download';
2
3
  export * from './lib/parseM3u8';
4
+ export * from './video-parser';
package/cjs/index.js CHANGED
@@ -15,4 +15,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./lib/m3u8-download"), exports);
18
+ __exportStar(require("./lib/file-download"), exports);
18
19
  __exportStar(require("./lib/parseM3u8"), exports);
20
+ __exportStar(require("./video-parser"), exports);
@@ -0,0 +1,2 @@
1
+ import type { M3u8DLOptions, M3u8DLResult } from '../types';
2
+ export declare function fileDownload(url: string, options: M3u8DLOptions): Promise<M3u8DLResult>;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fileDownload = fileDownload;
4
+ const node_path_1 = require("node:path");
5
+ const fe_utils_1 = require("@lzwme/fe-utils");
6
+ const console_log_colors_1 = require("console-log-colors");
7
+ const utils_js_1 = require("./utils.js");
8
+ const format_options_js_1 = require("./format-options.js");
9
+ async function fileDownload(url, options) {
10
+ utils_js_1.logger.debug('fileDownload', url, options);
11
+ [url, options] = (0, format_options_js_1.formatOptions)(url, options);
12
+ const startTime = Date.now();
13
+ const stats = {
14
+ url,
15
+ startTime,
16
+ progress: 0,
17
+ tsSuccess: 0,
18
+ tsFailed: 0,
19
+ tsCount: 0,
20
+ duration: 0,
21
+ durationDownloaded: 0,
22
+ downloadedSize: 0,
23
+ avgSpeed: 0,
24
+ avgSpeedDesc: '0B/s',
25
+ speed: 0,
26
+ speedDesc: '0B/s',
27
+ remainingTime: 0,
28
+ filename: options.filename,
29
+ localVideo: (0, node_path_1.resolve)(options.saveDir, options.filename),
30
+ };
31
+ utils_js_1.logger.debug('开始下载', (0, console_log_colors_1.gray)(url), (0, console_log_colors_1.cyan)(stats.localVideo));
32
+ if (options.onInited)
33
+ options.onInited(stats, null, null);
34
+ try {
35
+ const r = await (0, fe_utils_1.download)({
36
+ url,
37
+ filepath: stats.localVideo,
38
+ paralelism: options.threadNum,
39
+ force: options.force,
40
+ requestOptions: {
41
+ headers: {
42
+ referer: url,
43
+ ...(0, utils_js_1.formatHeaders)(options.headers),
44
+ },
45
+ rejectUnauthorized: false,
46
+ },
47
+ onProgress: info => {
48
+ stats.progress = +info.percent.toFixed(2);
49
+ stats.size = info.size;
50
+ stats.downloadedSize = info.downloaded;
51
+ stats.speed = info.speed;
52
+ stats.speedDesc = (0, fe_utils_1.formatByteSize)(stats.speed) + '/s';
53
+ stats.remainingTime = Math.round((info.size - info.downloaded) / stats.speed);
54
+ if (options.showProgress) {
55
+ const processBar = info.percent === -1 ? '' : '='.repeat(Math.floor(stats.progress * 0.2)).padEnd(20, '-');
56
+ utils_js_1.logger.logInline(`${stats.progress}% [${(0, console_log_colors_1.greenBright)(processBar)}] ` +
57
+ `${(0, console_log_colors_1.blueBright)((0, fe_utils_1.formatByteSize)(stats.downloadedSize))} ${(0, console_log_colors_1.yellowBright)((0, fe_utils_1.formatTimeCost)(startTime))} ${(0, console_log_colors_1.magentaBright)(stats.speedDesc)} ` +
58
+ (stats.progress === 100 ? '\n' : stats.remainingTime ? `${(0, console_log_colors_1.cyan)((0, fe_utils_1.formatTimeCost)(Date.now() - stats.remainingTime))}` : ''));
59
+ }
60
+ if (options.onProgress) {
61
+ return options.onProgress(info.downloaded, info.size, null, stats);
62
+ }
63
+ },
64
+ });
65
+ stats.endTime = Date.now();
66
+ return {
67
+ errmsg: r.filepath ? '下载完成' : '下载失败',
68
+ ...r,
69
+ stats,
70
+ };
71
+ }
72
+ catch (error) {
73
+ utils_js_1.logger.error('下载失败', error.message, (0, console_log_colors_1.gray)(url));
74
+ stats.errmsg = error.message;
75
+ return {
76
+ isExist: false,
77
+ errmsg: '下载失败: ' + error.message,
78
+ stats,
79
+ };
80
+ }
81
+ }
@@ -0,0 +1,2 @@
1
+ import { M3u8DLOptions } from '../types';
2
+ export declare function formatOptions(url: string, opts: M3u8DLOptions): readonly [string, M3u8DLOptions, string];
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatOptions = formatOptions;
4
+ const node_os_1 = require("node:os");
5
+ const node_path_1 = require("node:path");
6
+ const fe_utils_1 = require("@lzwme/fe-utils");
7
+ const utils_1 = require("./utils");
8
+ const video_parser_1 = require("../video-parser");
9
+ const fileSupportExtList = [
10
+ '.mp4',
11
+ '.mkv',
12
+ '.avi',
13
+ '.mov',
14
+ '.wmv',
15
+ '.ts',
16
+ '.exe',
17
+ '.zip',
18
+ '.rar',
19
+ '.pdf',
20
+ '.doc',
21
+ '.docx',
22
+ '.xls',
23
+ '.xlsx',
24
+ '.ppt',
25
+ '.pptx',
26
+ ];
27
+ function formatOptions(url, opts) {
28
+ const options = {
29
+ delCache: !opts.debug,
30
+ saveDir: process.cwd(),
31
+ showProgress: true,
32
+ ...opts,
33
+ };
34
+ let ext = options.filename ? (0, node_path_1.extname)(options.filename) : '';
35
+ if (!options.type) {
36
+ if (video_parser_1.VideoParser.getPlatform(url).platform !== 'unknown') {
37
+ options.type = 'parser';
38
+ }
39
+ else {
40
+ options.type = 'm3u8';
41
+ if (!url.includes('.m3u8')) {
42
+ const e = fileSupportExtList.find(d => url.includes(d));
43
+ if (e) {
44
+ options.type = 'file';
45
+ ext = e;
46
+ }
47
+ }
48
+ }
49
+ }
50
+ let [u, n] = url.split(/[|$]+/);
51
+ if (n && n.startsWith('http'))
52
+ [u, n] = [n, u];
53
+ url = u;
54
+ if (n) {
55
+ if (!options.filename)
56
+ options.filename = n;
57
+ else
58
+ options.filename = `${options.filename.replace(/\.(ts|mp4)$/, '')}-${n}${ext}`;
59
+ }
60
+ const urlMd5 = (0, fe_utils_1.md5)(url, false);
61
+ if (!options.filename) {
62
+ if (ext && url.includes(ext))
63
+ options.filename = (0, node_path_1.basename)(url.split(ext)[0]) + ext;
64
+ else
65
+ options.filename = urlMd5 + ext;
66
+ }
67
+ if (!(0, node_path_1.extname)(options.filename) && options.type !== 'file')
68
+ options.filename += ext || '.mp4';
69
+ if (!options.cacheDir)
70
+ options.cacheDir = `cache`;
71
+ if (options.headers)
72
+ options.headers = (0, utils_1.formatHeaders)(options.headers);
73
+ if (!options.threadNum || +options.threadNum <= 0)
74
+ options.threadNum = Math.min((0, node_os_1.cpus)().length * 2, 8);
75
+ if (options.debug) {
76
+ utils_1.logger.updateOptions({ levelType: 'debug' });
77
+ utils_1.logger.debug('[m3u8-DL]options', options, url);
78
+ }
79
+ return [url, options, urlMd5];
80
+ }
@@ -1,5 +1,4 @@
1
- import { parseM3U8 } from './parseM3u8.js';
2
- import type { M3u8DLOptions, M3u8WorkerPool } from '../types/m3u8.js';
1
+ import type { M3u8DLOptions, M3u8DLResult, M3u8WorkerPool } from '../types/m3u8.js';
3
2
  /** 下载队列管理 */
4
3
  export declare class DownloadQueue {
5
4
  private queue;
@@ -23,12 +22,5 @@ export declare class DownloadQueue {
23
22
  export declare const downloadQueue: DownloadQueue;
24
23
  export declare const workPollPublic: M3u8WorkerPool;
25
24
  export declare function preDownLoad(url: string, options: M3u8DLOptions, wp?: M3u8WorkerPool): Promise<void>;
26
- export declare function m3u8Download(url: string, options?: M3u8DLOptions): Promise<{
27
- filepath?: string;
28
- error?: Error;
29
- } | {
30
- options: M3u8DLOptions;
31
- m3u8Info: Awaited<ReturnType<typeof parseM3U8>> | null;
32
- filepath: string;
33
- }>;
25
+ export declare function m3u8Download(url: string, options?: M3u8DLOptions): Promise<M3u8DLResult>;
34
26
  export declare function m3u8DLStop(url: string, wp?: M3u8WorkerPool): number;
@@ -10,6 +10,7 @@ const fe_utils_1 = require("@lzwme/fe-utils");
10
10
  const helper_1 = require("@lzwme/fe-utils/cjs/common/helper");
11
11
  const console_log_colors_1 = require("console-log-colors");
12
12
  const utils_js_1 = require("./utils.js");
13
+ const format_options_js_1 = require("./format-options.js");
13
14
  const worker_pool_js_1 = require("./worker_pool.js");
14
15
  const parseM3u8_js_1 = require("./parseM3u8.js");
15
16
  const m3u8_convert_js_1 = require("./m3u8-convert.js");
@@ -43,11 +44,12 @@ class DownloadQueue {
43
44
  try {
44
45
  const { maxDownloads, ...options } = next.options;
45
46
  const result = await m3u8Download(next.url, options);
46
- next.options.onComplete?.({ filepath: result.filepath });
47
+ next.options.onComplete?.(result);
47
48
  }
48
49
  catch (error) {
49
50
  next.options.onComplete?.({
50
- error: error instanceof Error ? error : new Error(error ? JSON.stringify(error) : 'Unknown error'),
51
+ errmsg: error instanceof Error ? error.message : error ? JSON.stringify(error) : 'Unknown error',
52
+ options: next.options,
51
53
  });
52
54
  }
53
55
  finally {
@@ -85,7 +87,8 @@ const cache = {
85
87
  const tsDlFile = (0, node_path_1.resolve)(__dirname, './ts-download.js');
86
88
  exports.workPollPublic = new worker_pool_js_1.WorkerPool(tsDlFile);
87
89
  async function m3u8InfoParse(url, options = {}) {
88
- [url, options] = (0, utils_js_1.formatOptions)(url, options);
90
+ let urlMd5 = '';
91
+ [url, options, urlMd5] = (0, format_options_js_1.formatOptions)(url, options);
89
92
  const ext = (0, utils_js_1.isSupportFfmpeg)() ? '.mp4' : '.ts';
90
93
  /** 最终合并转换后的文件路径 */
91
94
  let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename);
@@ -98,7 +101,7 @@ async function m3u8InfoParse(url, options = {}) {
98
101
  }
99
102
  if (!options.force && (0, node_fs_1.existsSync)(filepath))
100
103
  return result;
101
- const m3u8Info = await (0, parseM3u8_js_1.parseM3U8)(url, (0, node_path_1.resolve)(options.cacheDir, (0, fe_utils_1.md5)(url, false)), options.headers).catch(e => utils_js_1.logger.error('[parseM3U8][failed]', e));
104
+ const m3u8Info = await (0, parseM3u8_js_1.parseM3U8)(url, (0, node_path_1.resolve)(options.cacheDir, urlMd5), options.headers).catch(e => utils_js_1.logger.error('[parseM3U8][failed]', e));
102
105
  if (m3u8Info && m3u8Info?.tsCount > 0)
103
106
  result.m3u8Info = m3u8Info;
104
107
  return result;
@@ -142,8 +145,10 @@ async function m3u8Download(url, options = {}) {
142
145
  return new Promise(resolve => {
143
146
  const newOptions = {
144
147
  ...options,
145
- onComplete: (result) => {
146
- resolve(result);
148
+ onComplete: r => {
149
+ if (options.onComplete)
150
+ options.onComplete(r);
151
+ resolve(r);
147
152
  },
148
153
  };
149
154
  exports.downloadQueue.add(url, newOptions, options.priority || 0);
@@ -155,6 +160,7 @@ async function m3u8Download(url, options = {}) {
155
160
  options = result.options;
156
161
  if (!options.force && (0, node_fs_1.existsSync)(result.filepath) && !result.m3u8Info) {
157
162
  utils_js_1.logger.info('file already exist:', result.filepath);
163
+ result.isExist = true;
158
164
  return result;
159
165
  }
160
166
  if (result.m3u8Info?.tsCount > 0) {
@@ -261,6 +267,7 @@ async function m3u8Download(url, options = {}) {
261
267
  if (options.showProgress) {
262
268
  console.info(`\nTotal segments: ${(0, console_log_colors_1.cyan)(m3u8Info.tsCount)}, duration: ${(0, console_log_colors_1.green)(m3u8Info.duration + 'sec')}.`, `Parallel jobs: ${(0, console_log_colors_1.magenta)(options.threadNum)}`);
263
269
  }
270
+ result.stats = stats;
264
271
  (0, local_play_js_1.toLocalM3u8)(m3u8Info.data, options.filename);
265
272
  if (options.onInited)
266
273
  options.onInited(stats, m3u8Info, workPoll);
@@ -1,7 +1,6 @@
1
1
  import type { IncomingHttpHeaders } from 'node:http';
2
2
  import { Stats } from 'node:fs';
3
3
  import { NLogger, Request } from '@lzwme/fe-utils';
4
- import { M3u8DLOptions } from '../types';
5
4
  export declare const request: Request;
6
5
  export declare const getRetry: <T = string>(url: string, headers?: IncomingHttpHeaders, retries?: number) => Promise<{
7
6
  data: T;
@@ -19,4 +18,3 @@ export declare function getLocation(url: string, method?: string): Promise<strin
19
18
  * 如果 headers 是字符串,会先将其解析为对象;如果 headers 为空,则返回空对象。
20
19
  */
21
20
  export declare function formatHeaders(headers: string | IncomingHttpHeaders): Record<string, string>;
22
- export declare function formatOptions(url: string, opts: M3u8DLOptions): readonly [string, M3u8DLOptions];
package/cjs/lib/utils.js CHANGED
@@ -5,8 +5,6 @@ exports.isSupportFfmpeg = isSupportFfmpeg;
5
5
  exports.findFiles = findFiles;
6
6
  exports.getLocation = getLocation;
7
7
  exports.formatHeaders = formatHeaders;
8
- exports.formatOptions = formatOptions;
9
- const node_os_1 = require("node:os");
10
8
  const node_fs_1 = require("node:fs");
11
9
  const node_path_1 = require("node:path");
12
10
  const fe_utils_1 = require("@lzwme/fe-utils");
@@ -68,38 +66,3 @@ function formatHeaders(headers) {
68
66
  return {};
69
67
  return (0, fe_utils_1.toLowcaseKeyObject)(headers);
70
68
  }
71
- function formatOptions(url, opts) {
72
- const options = {
73
- delCache: !opts.debug,
74
- saveDir: process.cwd(),
75
- showProgress: true,
76
- ...opts,
77
- };
78
- if (!url.startsWith('http')) {
79
- url = url.replace(/\$+/, '|').replace(/\|\|+/, '|');
80
- if (url.includes('|')) {
81
- const r = url.split('|');
82
- url = r[1];
83
- if (!options.filename)
84
- options.filename = r[0];
85
- else
86
- options.filename = `${options.filename.replace(/\.(ts|mp4)$/, '')}-${r[0]}`;
87
- }
88
- }
89
- const urlMd5 = (0, fe_utils_1.md5)(url, false);
90
- if (!options.threadNum || +options.threadNum <= 0)
91
- options.threadNum = Math.min((0, node_os_1.cpus)().length * 2, 8);
92
- if (!options.filename)
93
- options.filename = urlMd5;
94
- if (!options.filename.endsWith('.mp4'))
95
- options.filename += '.mp4';
96
- if (!options.cacheDir)
97
- options.cacheDir = `cache`;
98
- if (options.headers)
99
- options.headers = formatHeaders(options.headers);
100
- if (options.debug) {
101
- exports.logger.updateOptions({ levelType: 'debug' });
102
- exports.logger.debug('[m3u8-DL]options', options, url);
103
- }
104
- return [url, options];
105
- }
@@ -5,7 +5,7 @@ exports.m3u8BatchDownload = m3u8BatchDownload;
5
5
  * @Author: renxia lzwy0820@qq.com
6
6
  * @Date: 2024-07-30 08:57:58
7
7
  * @LastEditors: renxia
8
- * @LastEditTime: 2025-05-09 16:59:23
8
+ * @LastEditTime: 2025-05-19 17:03:50
9
9
  * @FilePath: \m3u8-dl\src\m3u8-batch-download.ts
10
10
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
11
11
  */
@@ -15,9 +15,11 @@ const m3u8_download_1 = require("./lib/m3u8-download");
15
15
  const utils_1 = require("./lib/utils");
16
16
  const video_parser_1 = require("./video-parser");
17
17
  const console_log_colors_1 = require("console-log-colors");
18
+ const file_download_1 = require("./lib/file-download");
19
+ const format_options_1 = require("./lib/format-options");
18
20
  async function formatUrls(urls, options) {
19
21
  const taskset = new Map();
20
- for (const url of urls) {
22
+ for (let url of urls) {
21
23
  if (!url)
22
24
  continue;
23
25
  if ((0, node_fs_1.existsSync)(url)) {
@@ -40,6 +42,7 @@ async function formatUrls(urls, options) {
40
42
  continue;
41
43
  }
42
44
  }
45
+ [url, options] = (0, format_options_1.formatOptions)(url, options);
43
46
  taskset.set(url, options);
44
47
  }
45
48
  return taskset;
@@ -49,50 +52,52 @@ async function m3u8BatchDownload(urls, options) {
49
52
  let workPoll;
50
53
  return new Promise(rs => {
51
54
  let preDLing = false;
55
+ const afterDownload = (r, url) => {
56
+ const success = r.filepath && (0, node_fs_1.existsSync)(r.filepath);
57
+ if (success) {
58
+ if (r.isExist)
59
+ utils_1.logger.info('文件已存在:', (0, console_log_colors_1.gray)(r.filepath));
60
+ utils_1.logger.debug('下载完成:', (0, console_log_colors_1.gray)(url), (0, console_log_colors_1.gray)(r.filepath));
61
+ }
62
+ else {
63
+ utils_1.logger.error('下载失败:', (0, console_log_colors_1.red)(r.errmsg || '未知错误'), (0, console_log_colors_1.gray)(url));
64
+ }
65
+ if (tasks.size === 0)
66
+ rs(r.filepath && (0, node_fs_1.existsSync)(r.filepath));
67
+ else
68
+ run();
69
+ };
52
70
  const run = () => {
53
71
  const [url, urlNext] = [...tasks.keys()];
54
72
  if (url) {
55
- const o = { ...tasks.get(url) };
73
+ const o = tasks.get(url);
56
74
  const onProgress = o.onProgress;
57
- const useVideoParser = video_parser_1.VideoParser.getPlatform(url) !== null;
58
75
  tasks.delete(url);
59
76
  o.onInited = (s, _i, wp) => {
60
77
  if (workPoll)
61
78
  workPoll = wp;
62
- if (useVideoParser) {
79
+ if (o.type === 'parser') {
63
80
  utils_1.logger.info('视频解析完成:', s.filename, (0, console_log_colors_1.gray)(s.url));
64
81
  }
65
82
  };
66
83
  o.onProgress = (finished, total, info, stats) => {
67
84
  if (onProgress)
68
85
  onProgress(finished, total, info, stats);
69
- if (!useVideoParser && !preDLing && urlNext && tasks.size && workPoll.freeNum > 1 && total - finished < options.threadNum) {
86
+ if (o.type === 'm3u8' && !preDLing && urlNext && tasks.size && workPoll.freeNum > 1 && total - finished < options.threadNum) {
70
87
  utils_1.logger.debug('\n[预下载下一集]', 'freeNum:', workPoll.freeNum, 'totalNum:', workPoll.totalNum, 'totalTask:', workPoll.totalTask, tasks.size);
71
88
  preDLing = true;
72
89
  (0, m3u8_download_1.preDownLoad)(urlNext, options, workPoll).then(() => (preDLing = false));
73
90
  }
74
91
  };
75
- if (useVideoParser) {
92
+ if (o.type === 'parser') {
76
93
  const vp = new video_parser_1.VideoParser();
77
- vp.download(url, o).then(r => {
78
- if (r.code !== 0) {
79
- utils_1.logger.error('下载失败:', (0, console_log_colors_1.red)(r.message), (0, console_log_colors_1.gray)(url));
80
- }
81
- else {
82
- if (r.data?.isExist)
83
- utils_1.logger.info('文件已存在:', (0, console_log_colors_1.gray)(r.data.filepath));
84
- utils_1.logger.debug('下载完成:', (0, console_log_colors_1.gray)(url), (0, console_log_colors_1.gray)(r.data.filepath));
85
- }
86
- if (tasks.size === 0) {
87
- rs(r.data.filepath && (0, node_fs_1.existsSync)(r.data.filepath));
88
- }
89
- else {
90
- run();
91
- }
92
- });
94
+ vp.download(url, o).then(r => afterDownload(r, url));
95
+ }
96
+ else if (o.type === 'file') {
97
+ (0, file_download_1.fileDownload)(url, o).then(r => afterDownload(r, url));
93
98
  }
94
99
  else {
95
- (0, m3u8_download_1.m3u8Download)(url, o).then(r => (tasks.size === 0 ? rs((0, node_fs_1.existsSync)(r.filepath)) : run()));
100
+ (0, m3u8_download_1.m3u8Download)(url, o).then(r => afterDownload(r, url));
96
101
  }
97
102
  }
98
103
  };
@@ -11,7 +11,10 @@ interface DLServerOptions {
11
11
  }
12
12
  interface CacheItem extends Partial<M3u8DLProgressStats> {
13
13
  url: string;
14
+ /** 用户设置的参数 */
14
15
  options: M3u8DLOptions;
16
+ /** 格式化后实际下载使用的参数 */
17
+ dlOptions?: M3u8DLOptions;
15
18
  status: 'pause' | 'resume' | 'done' | 'pending' | 'error';
16
19
  current?: TsItemInfo;
17
20
  workPoll?: M3u8WorkerPool;
@@ -34,11 +34,6 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.DLServer = void 0;
37
- /*
38
- * @Author: renxia lzwy0820@qq.com
39
- * @Date: 2025-05-07 21:03:09
40
- * @LastEditors: renxia
41
- */
42
37
  const node_fs_1 = require("node:fs");
43
38
  const node_path_1 = require("node:path");
44
39
  const fe_utils_1 = require("@lzwme/fe-utils");
@@ -46,6 +41,8 @@ const m3u8_download_js_1 = require("../lib/m3u8-download.js");
46
41
  const utils_js_1 = require("../lib/utils.js");
47
42
  const index_js_1 = require("../video-parser/index.js");
48
43
  const console_log_colors_1 = require("console-log-colors");
44
+ const file_download_js_1 = require("../lib/file-download.js");
45
+ const format_options_js_1 = require("../lib/format-options.js");
49
46
  class DLServer {
50
47
  app = null;
51
48
  wss = null;
@@ -74,6 +71,7 @@ class DLServer {
74
71
  dlOptions: {
75
72
  debug: process.env.DS_DEBUG == '1',
76
73
  saveDir: process.env.DS_SAVE_DIR || './downloads',
74
+ threadNum: 4,
77
75
  },
78
76
  };
79
77
  /** 下载任务缓存 */
@@ -175,11 +173,10 @@ class DLServer {
175
173
  const { default: express } = await Promise.resolve().then(() => __importStar(require('express')));
176
174
  const { WebSocketServer } = await Promise.resolve().then(() => __importStar(require('ws')));
177
175
  const app = (this.app = express());
178
- const server = app.listen(this.options.port, () => utils_js_1.logger.info(`Server running on port ${this.options.port}`));
176
+ const server = app.listen(this.options.port, () => utils_js_1.logger.info(`Server running on port ${(0, console_log_colors_1.green)(this.options.port)}`));
179
177
  const wss = (this.wss = new WebSocketServer({ server }));
180
178
  app.use(express.json());
181
179
  app.use(express.static((0, node_path_1.resolve)(__dirname, '../../client')));
182
- // headers
183
180
  app.use((req, res, next) => {
184
181
  res.setHeader('Access-Control-Allow-Origin', '*');
185
182
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
@@ -213,28 +210,15 @@ class DLServer {
213
210
  }
214
211
  ws.send(JSON.stringify({ type: 'serverInfo', data: this.serverInfo }));
215
212
  ws.send(JSON.stringify({ type: 'tasks', data: Object.fromEntries(this.dlCacheClone()) }));
216
- // ws.on('message', (message, _isBinary) => {
217
- // logger.info('Received message from client:', (message as Buffer).toString('utf8'));
218
- // });
219
- });
220
- wss.on('close', () => {
221
- utils_js_1.logger.info('WebSocket server closed');
222
- });
223
- wss.on('error', err => {
224
- utils_js_1.logger.error('WebSocket server error:', err);
225
- });
226
- wss.on('listening', () => {
227
- utils_js_1.logger.info(`WebSocket server listening on port ${this.options.port}`);
228
213
  });
214
+ wss.on('close', () => utils_js_1.logger.info('WebSocket server closed'));
215
+ wss.on('error', err => utils_js_1.logger.error('WebSocket server error:', err));
216
+ wss.on('listening', () => utils_js_1.logger.info(`WebSocket server listening on port ${(0, console_log_colors_1.green)(this.options.port)}`));
229
217
  return { app, wss };
230
218
  }
231
219
  startDownload(url, options) {
232
220
  const cacheItem = this.dlCache.get(url);
233
- const dlOptions = {
234
- ...this.cfg.dlOptions,
235
- ...options,
236
- cacheDir: this.options.cacheDir,
237
- };
221
+ const dlOptions = (0, format_options_js_1.formatOptions)(url, { ...this.cfg.dlOptions, ...options, cacheDir: this.options.cacheDir })[1];
238
222
  utils_js_1.logger.debug('startDownload', url, dlOptions, cacheItem?.status);
239
223
  if (cacheItem && cacheItem?.status === 'resume')
240
224
  return cacheItem.options;
@@ -242,12 +226,11 @@ class DLServer {
242
226
  if (cacheItem)
243
227
  cacheItem.status = 'pending';
244
228
  else
245
- this.dlCache.set(url, { options: dlOptions, status: 'pending', url });
229
+ this.dlCache.set(url, { options, dlOptions, status: 'pending', url });
246
230
  return cacheItem?.options || dlOptions;
247
231
  }
248
232
  let workPoll = cacheItem?.workPoll;
249
- const useVideoParser = index_js_1.VideoParser.getPlatform(url) !== null;
250
- const defaultItem = { options: dlOptions, status: 'resume', url };
233
+ const defaultItem = { options, dlOptions, status: 'resume', url };
251
234
  const opts = {
252
235
  ...dlOptions,
253
236
  showProgress: dlOptions.debug || this.options.debug,
@@ -259,20 +242,21 @@ class DLServer {
259
242
  this.dlCache.set(url, item);
260
243
  this.saveCache();
261
244
  this.wsSend('progress', url);
245
+ return status !== 'pause';
262
246
  },
263
247
  };
264
- const afterDownload = ({ filepath = '', errmsg = '' }) => {
248
+ const afterDownload = (r, url) => {
265
249
  const item = this.dlCache.get(url) || defaultItem;
266
- if (filepath && (0, node_fs_1.existsSync)(filepath)) {
267
- item.localVideo = filepath;
268
- item.downloadedSize = (0, node_fs_1.statSync)(filepath).size;
250
+ if (r.filepath && (0, node_fs_1.existsSync)(r.filepath)) {
251
+ item.localVideo = r.filepath;
252
+ item.downloadedSize = (0, node_fs_1.statSync)(r.filepath).size;
269
253
  }
270
- else if (!errmsg && opts.convert !== false)
271
- errmsg = '下载失败';
254
+ else if (!r.errmsg && opts.convert !== false)
255
+ r.errmsg = '下载失败';
272
256
  item.endTime = Date.now();
273
- item.status = errmsg ? 'error' : 'done';
274
- item.errmsg = errmsg;
275
- utils_js_1.logger.info('Download complete:', item.status, (0, console_log_colors_1.red)(item.errmsg), (0, console_log_colors_1.gray)(url), (0, console_log_colors_1.cyan)(filepath));
257
+ item.errmsg = r.errmsg;
258
+ item.status = r.errmsg ? 'error' : 'done';
259
+ utils_js_1.logger.info('Download complete:', item.status, (0, console_log_colors_1.red)(r.errmsg), (0, console_log_colors_1.gray)(url), (0, console_log_colors_1.cyan)(r.filepath));
276
260
  this.dlCache.set(url, item);
277
261
  this.wsSend('progress', url);
278
262
  this.saveCache();
@@ -288,21 +272,19 @@ class DLServer {
288
272
  if (cacheItem)
289
273
  cacheItem.status = 'resume';
290
274
  try {
291
- if (useVideoParser) {
275
+ if (dlOptions.type === 'parser') {
292
276
  const vp = new index_js_1.VideoParser();
293
- vp.download(url, opts).then(r => {
294
- afterDownload({ filepath: r.data?.filepath, errmsg: r.code ? r.message : '' });
295
- });
277
+ vp.download(url, opts).then(r => afterDownload(r, url));
278
+ }
279
+ else if (dlOptions.type === 'file') {
280
+ (0, file_download_js_1.fileDownload)(url, opts).then(r => afterDownload(r, url));
296
281
  }
297
282
  else {
298
- (0, m3u8_download_js_1.m3u8Download)(url, opts).then(r => {
299
- const errmsg = 'error' in r ? r.error.cause || r.error.message : '';
300
- afterDownload({ filepath: r.filepath, errmsg });
301
- });
283
+ (0, m3u8_download_js_1.m3u8Download)(url, opts).then(r => afterDownload(r, url));
302
284
  }
303
285
  }
304
286
  catch (error) {
305
- afterDownload({ filepath: '', errmsg: error.message });
287
+ afterDownload({ filepath: '', errmsg: error.message }, url);
306
288
  utils_js_1.logger.error('下载失败:', error);
307
289
  }
308
290
  return dlOptions;
@@ -315,8 +297,10 @@ class DLServer {
315
297
  const item = this.dlCache.get(data);
316
298
  if (item) {
317
299
  const { workPoll, ...stats } = item;
318
- data = { ...stats, url: data };
300
+ data = [{ ...stats, url: data }];
319
301
  }
302
+ else
303
+ return;
320
304
  }
321
305
  // 广播进度信息给所有客户端
322
306
  this.wss.clients.forEach(client => {
@@ -330,13 +314,11 @@ class DLServer {
330
314
  app.get('/healthcheck', (_req, res) => {
331
315
  res.json({ message: 'ok', code: 0 });
332
316
  });
333
- // API to set default config
334
317
  app.post('/config', (req, res) => {
335
318
  const config = req.body;
336
319
  this.saveConfig(config);
337
320
  res.json({ message: 'Config updated successfully', code: 0 });
338
321
  });
339
- // API to get default config
340
322
  app.get('/config', (_req, res) => {
341
323
  res.json({ ...this.cfg.dlOptions, ...this.cfg.webOptions });
342
324
  });
@@ -405,9 +387,9 @@ class DLServer {
405
387
  const urlsToPause = all ? [...this.dlCache.keys()] : urls;
406
388
  const list = [];
407
389
  for (const url of urlsToPause) {
408
- const item = this.dlCache.get(url);
409
- if (item?.status === 'resume') {
410
- (0, m3u8_download_js_1.m3u8DLStop)(url, item.workPoll);
390
+ const { workPoll, ...item } = this.dlCache.get(url);
391
+ if (['resume', 'pending'].includes(item?.status)) {
392
+ (0, m3u8_download_js_1.m3u8DLStop)(url, workPoll);
411
393
  item.status = item.tsSuccess === item.tsCount ? 'done' : 'pause';
412
394
  list.push(item);
413
395
  }
@@ -425,15 +407,15 @@ class DLServer {
425
407
  const item = this.dlCache.get(url);
426
408
  if (['pause', 'error'].includes(item?.status)) {
427
409
  this.startDownload(url, item.options);
428
- list.push(item);
410
+ const { workPoll, ...t } = item;
411
+ list.push(t);
429
412
  }
430
413
  else
431
414
  console.log(item?.status, url);
432
415
  }
433
- const count = list.length;
434
- if (count)
416
+ if (list.length)
435
417
  this.wsSend('progress', list);
436
- res.json({ message: count ? `已恢复 ${count} 个下载任务` : '没有找到可恢复的下载任务', code: count ? 0 : 1 });
418
+ res.json({ message: list.length ? `已恢复 ${list.length} 个下载任务` : '没有找到可恢复的下载任务', code: 0, count: list.length });
437
419
  });
438
420
  // API to delete download
439
421
  app.post('/delete', (req, res) => {
@@ -445,6 +427,7 @@ class DLServer {
445
427
  if (item) {
446
428
  (0, m3u8_download_js_1.m3u8DLStop)(url, item.workPoll);
447
429
  this.dlCache.delete(url);
430
+ list.push(item.url);
448
431
  if (deleteCache && item.current?.tsOut) {
449
432
  const cacheDir = (0, node_path_1.dirname)(item.current.tsOut);
450
433
  if ((0, node_fs_1.existsSync)(cacheDir))
@@ -457,13 +440,11 @@ class DLServer {
457
440
  (0, node_fs_1.unlinkSync)(filepath);
458
441
  });
459
442
  }
460
- list.push(item);
461
443
  }
462
444
  }
463
- const count = list.length;
464
- if (count)
465
- this.wsSend('progress', list);
466
- res.json({ message: `已删除 ${count} 个下载任务`, code: 0, count });
445
+ if (list.length)
446
+ this.wsSend('delete', list);
447
+ res.json({ message: `已删除 ${list.length} 个下载任务`, code: 0, count: list.length });
467
448
  });
468
449
  app.get(/^\/localplay\/(.*)$/, (req, res) => {
469
450
  let filepath = decodeURIComponent(req.params[0]);
@@ -1,4 +1,4 @@
1
- import type { AnyObject } from '@lzwme/fe-utils';
1
+ import type { AnyObject, DownloadResult } from '@lzwme/fe-utils';
2
2
  import type { IncomingHttpHeaders } from 'node:http';
3
3
  import type { WorkerPool } from '../lib/worker_pool';
4
4
  export interface TsItemInfo {
@@ -100,12 +100,9 @@ export interface M3u8DLOptions {
100
100
  /** 当初始化完成、下载开始时回调 */
101
101
  onInited?: (stats: M3u8DLProgressStats, m3u8Info: M3u8Info, workPoll: M3u8WorkerPool) => void;
102
102
  /** 每当 ts 文件下载完成时回调,可用于自定义进度控制 */
103
- onProgress?: (finished: number, total: number, currentInfo: TsItemInfo, stats: M3u8DLProgressStats) => void;
103
+ onProgress?: (finished: number, total: number, currentInfo: TsItemInfo, stats: M3u8DLProgressStats) => void | boolean;
104
104
  /** 下载完成时回调,主要用于内部多任务管理 */
105
- onComplete?: (result: {
106
- error?: Error;
107
- filepath?: string;
108
- }) => void;
105
+ onComplete?: (result: M3u8DLResult) => void;
109
106
  /** 并发下载线程数。取决于服务器限制,过多可能会容易下载失败。一般建议不超过 8 个。默认为 cpu数 * 2,但不超过 8 */
110
107
  threadNum?: number;
111
108
  /** 最大并发下载任务数 */
@@ -128,6 +125,21 @@ export interface M3u8DLOptions {
128
125
  play?: boolean;
129
126
  /** 下载完毕后,是否合并转换为 mp4 或 ts 文件。默认为 true */
130
127
  convert?: boolean;
128
+ /**
129
+ * 下载类型。默认自动识别
130
+ * - 'm3u8':下载 m3u8 文件
131
+ * - 'file':下载普通文件
132
+ * - 'parser':下载 VideoParser 支持解析的平台视频文件
133
+ */
134
+ type?: 'm3u8' | 'file' | 'parser';
135
+ }
136
+ export interface M3u8DLResult extends Partial<DownloadResult> {
137
+ /** 下载进度统计 */
138
+ stats?: M3u8DLProgressStats;
139
+ /** 下载选项 */
140
+ options?: M3u8DLOptions;
141
+ /** m3u8 文件信息 */
142
+ m3u8Info?: M3u8Info;
131
143
  }
132
144
  export interface WorkerTaskInfo {
133
145
  /** m3u8 文件地址 */
@@ -1,12 +1,12 @@
1
1
  export interface VideoInfo {
2
- author: string;
3
- uid: string;
4
- avatar: string;
5
- like: number;
6
- time: number;
7
- title: string;
8
- cover: string;
9
2
  url: string;
3
+ title: string;
4
+ author?: string;
5
+ avatar?: string;
6
+ time?: number;
7
+ cover?: string;
8
+ uid?: string;
9
+ like?: number;
10
10
  /** 来源页面 */
11
11
  referer?: string;
12
12
  music?: {
@@ -1,14 +1,11 @@
1
- import { type DownloadResult } from '@lzwme/fe-utils';
2
- import type { ApiResponse, M3u8DLOptions, M3u8DLProgressStats, VideoInfo } from '../types';
1
+ import type { ApiResponse, M3u8DLOptions, M3u8DLResult, VideoInfo } from '../types';
3
2
  export declare class VideoParser {
4
3
  private static readonly platforms;
5
4
  /**
6
5
  * 解析视频 URL
7
6
  */
8
7
  parse(url: string, headers?: Record<string, string>): Promise<ApiResponse<VideoInfo>>;
9
- download(url: string, options: M3u8DLOptions): Promise<ApiResponse<DownloadResult> & {
10
- stats?: M3u8DLProgressStats;
11
- }>;
8
+ download(url: string, options: M3u8DLOptions): Promise<M3u8DLResult>;
12
9
  /**
13
10
  * 根据 URL 获取平台标识
14
11
  */
@@ -3,12 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VideoParser = void 0;
4
4
  const url_1 = require("url");
5
5
  const node_path_1 = require("node:path");
6
- const fe_utils_1 = require("@lzwme/fe-utils");
7
- const console_log_colors_1 = require("console-log-colors");
8
6
  const douyin_parser_1 = require("./parsers/douyin-parser");
9
7
  const pipixia_parser_1 = require("./parsers/pipixia-parser");
10
8
  const weibo_parser_1 = require("./parsers/weibo-parser");
9
+ const base_parser_1 = require("./parsers/base-parser");
11
10
  const utils_1 = require("../lib/utils");
11
+ const file_download_1 = require("../lib/file-download");
12
12
  class VideoParser {
13
13
  static platforms = {
14
14
  pipixia: {
@@ -23,6 +23,10 @@ class VideoParser {
23
23
  class: weibo_parser_1.WeiboParser,
24
24
  domains: ['weibo.com'],
25
25
  },
26
+ unknown: {
27
+ class: base_parser_1.BaseParser,
28
+ domains: ['**'],
29
+ },
26
30
  };
27
31
  /**
28
32
  * 解析视频 URL
@@ -38,83 +42,19 @@ class VideoParser {
38
42
  const info = await this.parse(url);
39
43
  utils_1.logger.debug('解析视频信息', info);
40
44
  if (info.code || !info.data?.url)
41
- return { code: 1, ...info, data: null };
45
+ return { errmsg: info.message || '解析视频信息失败', options };
42
46
  if (!options.filename && info.data.title) {
43
47
  options.filename = info.data.title.replaceAll(/[\s\\/:*?"<>|]/g, '_');
44
48
  }
45
- [url, options] = (0, utils_1.formatOptions)(info.data.url, options);
49
+ if (!options.type)
50
+ options.type = 'parser';
46
51
  if (!(0, node_path_1.extname)(options.filename))
47
52
  options.filename += '.mp4';
48
- const startTime = Date.now();
49
- const stats = {
50
- url,
51
- startTime,
52
- progress: 0,
53
- tsSuccess: 0,
54
- tsFailed: 0,
55
- tsCount: 0,
56
- duration: 0,
57
- durationDownloaded: 0,
58
- downloadedSize: 0,
59
- avgSpeed: 0,
60
- avgSpeedDesc: '0B/s',
61
- speed: 0,
62
- speedDesc: '0B/s',
63
- remainingTime: 0,
64
- filename: options.filename,
65
- localVideo: (0, node_path_1.resolve)(options.saveDir, options.filename),
53
+ options.headers = {
54
+ referer: info.data.referer || info.data.url,
55
+ ...(0, utils_1.formatHeaders)(options.headers),
66
56
  };
67
- utils_1.logger.debug('开始下载', (0, console_log_colors_1.gray)(url), (0, console_log_colors_1.cyan)(stats.localVideo));
68
- if (options.onInited)
69
- options.onInited(stats, null, null);
70
- try {
71
- const r = await (0, fe_utils_1.download)({
72
- url,
73
- filepath: stats.localVideo,
74
- paralelism: options.threadNum,
75
- force: options.force,
76
- requestOptions: {
77
- headers: {
78
- referer: info.data.referer || info.data.url,
79
- ...(0, utils_1.formatHeaders)(options.headers),
80
- },
81
- rejectUnauthorized: false,
82
- },
83
- onProgress: info => {
84
- stats.progress = +info.percent.toFixed(2);
85
- stats.size = info.size;
86
- stats.downloadedSize = info.downloaded;
87
- stats.speed = info.speed;
88
- stats.speedDesc = (0, fe_utils_1.formatByteSize)(stats.speed) + '/s';
89
- stats.remainingTime = Math.round((info.size - info.downloaded) / stats.speed);
90
- if (options.showProgress) {
91
- const processBar = info.percent === -1 ? '' : '='.repeat(Math.floor(stats.progress * 0.2)).padEnd(20, '-');
92
- utils_1.logger.logInline(`${stats.progress}% [${(0, console_log_colors_1.greenBright)(processBar)}] ` +
93
- `${(0, console_log_colors_1.blueBright)((0, fe_utils_1.formatByteSize)(stats.downloadedSize))} ${(0, console_log_colors_1.yellowBright)((0, fe_utils_1.formatTimeCost)(startTime))} ${(0, console_log_colors_1.magentaBright)(stats.speedDesc)} ` +
94
- (stats.progress === 100 ? '\n' : stats.remainingTime ? `${(0, console_log_colors_1.cyan)((0, fe_utils_1.formatTimeCost)(Date.now() - stats.remainingTime))}` : ''));
95
- }
96
- if (options.onProgress) {
97
- options.onProgress(info.downloaded, info.size, null, stats);
98
- }
99
- },
100
- });
101
- stats.endTime = Date.now();
102
- return {
103
- code: r.filepath ? 0 : 201,
104
- message: r.filepath ? '下载完成' : '下载失败',
105
- data: r,
106
- stats,
107
- };
108
- }
109
- catch (error) {
110
- utils_1.logger.error('下载失败', error.message, (0, console_log_colors_1.gray)(url));
111
- stats.errmsg = error.message;
112
- return {
113
- code: 201,
114
- message: '下载失败: ' + error.message,
115
- stats,
116
- };
117
- }
57
+ return (0, file_download_1.fileDownload)(url, options);
118
58
  }
119
59
  /**
120
60
  * 根据 URL 获取平台标识
@@ -129,12 +69,12 @@ class VideoParser {
129
69
  return { url, platform };
130
70
  }
131
71
  }
72
+ return { url, platform: 'unknown' };
132
73
  }
133
74
  catch (error) {
134
75
  console.error('解析 URL 失败', url, error);
135
76
  return null;
136
77
  }
137
- return null;
138
78
  }
139
79
  /**
140
80
  * 获取所有支持的平台列表
@@ -2,5 +2,5 @@ import type { ApiResponse, VideoInfo } from '../../types';
2
2
  export declare abstract class BaseParser {
3
3
  protected static success<T>(data: T): ApiResponse<T>;
4
4
  protected static error(code: number, message: string): ApiResponse<null>;
5
- static parse(_url: string, _headers?: Record<string, string>): Promise<ApiResponse<VideoInfo>>;
5
+ static parse(url: string, headers?: Record<string, string>): Promise<ApiResponse<VideoInfo>>;
6
6
  }
@@ -3,21 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BaseParser = void 0;
4
4
  class BaseParser {
5
5
  static success(data) {
6
- return {
7
- code: 0,
8
- message: 'success',
9
- data,
10
- };
6
+ return { code: 0, message: 'success', data };
11
7
  }
12
8
  static error(code, message) {
13
- return {
14
- code,
15
- message,
16
- data: null,
17
- };
9
+ return { code, message, data: null };
18
10
  }
19
- static parse(_url, _headers = {}) {
20
- return Promise.resolve(this.success(null));
11
+ static parse(url, headers = {}) {
12
+ return Promise.resolve(this.success({ url, title: '', referer: headers.referer }));
21
13
  }
22
14
  }
23
15
  exports.BaseParser = BaseParser;
package/client/index.html CHANGED
@@ -426,7 +426,7 @@ services:
426
426
  <div class="flex items-center">
427
427
  <input type="checkbox" :checked="selectedTasks.includes(task.url)" @change="toggleTaskSelection(task.url)"
428
428
  class="form-checkbox h-5 w-5 text-blue-500 rounded focus:ring-blue-500 mr-2"
429
- :aria-label="'选择任务:' + (task.localVideo || task.filename || task.url)">
429
+ title="'选择任务:' + (task.localVideo || task.filename || task.url)">
430
430
  <h3 class="font-bold text-green-600 truncate" :title="task.url">
431
431
  {{ task.localVideo || task.filename || task.url }}
432
432
  </h3>
@@ -601,7 +601,7 @@ services:
601
601
  };
602
602
 
603
603
  Vue.prototype.T = T;
604
- new Vue({
604
+ window.APP = new Vue({
605
605
  el: '#app',
606
606
  data: {
607
607
  ws: null,
@@ -691,9 +691,12 @@ services:
691
691
  forceUpdate: function () {
692
692
  const now = Date.now();
693
693
  if (now - this.forceUpdateTime > 500) {
694
- this.$forceUpdate();
695
694
  this.forceUpdateTime = now;
696
- } else this.forceUpdate();
695
+ this.$forceUpdate();
696
+ } else {
697
+ if (this.forceUpdateTimeout) clearTimeout(this.forceUpdateTimeout);
698
+ this.forceUpdateTimeout = setTimeout(() => this.forceUpdate(), 500);
699
+ }
697
700
  },
698
701
  wsConnect: function (reconnectDelay = 3000) {
699
702
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
@@ -716,9 +719,15 @@ services:
716
719
  if (!Array.isArray(data)) data = [data];
717
720
  this.$nextTick(() => {
718
721
  data.forEach(item => item.url && (this.tasks[item.url] = item));
719
- // this.forceUpdate();
722
+ this.forceUpdate();
720
723
  });
721
724
  break;
725
+ case 'delete':
726
+ if (Array.isArray(data)) {
727
+ data.forEach(url => delete this.tasks[url]);
728
+ this.forceUpdate();
729
+ }
730
+ break;
722
731
  case 'queueStatus':
723
732
  this.queueStatus = data;
724
733
  break;
@@ -928,7 +937,7 @@ services:
928
937
  },
929
938
  /** 边下边播 */
930
939
  localPlay: function (task) {
931
- const url = location.origin + '/localplay/' + encodeURIComponent(task.localVideo || task.localM3u8);
940
+ const url = location.origin + '/localplay/' + (task.localVideo || task.localM3u8);
932
941
  console.log(task);
933
942
  // return window.open(`./play.html?url=${encodeURIComponent(url)}`);
934
943
  Swal.fire({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzwme/m3u8-dl",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
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",