@lzwme/m3u8-dl 1.1.3 → 1.2.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/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) 部署:
@@ -152,6 +152,7 @@ services:
152
152
  DS_PORT: '6600'
153
153
  DS_SAVE_DIR: '/app/downloads'
154
154
  DS_CACHE_DIR: '/app/cache'
155
+ DS_SECRET: '' # 设置访问密码
155
156
  DS_DEBUG: ''
156
157
  # command: >
157
158
  # sh -c "node cjs/server/index.js"
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);
@@ -82,11 +83,12 @@ commander_1.program
82
83
  .command('server')
83
84
  .description('启动下载中心web服务')
84
85
  .option('-P, --port <port>', '指定web服务端口。默认为6600')
85
- .option('--token <token>', '指定web服务密码(请求头authorization)。默认为空')
86
+ .option('-t, --token <token>', '指定web服务密码(请求头authorization)。默认为空')
86
87
  .action((options) => {
87
88
  const opts = getOptions();
88
89
  if (opts.debug)
89
90
  options.debug = true;
91
+ console.log(opts, options);
90
92
  Promise.resolve().then(() => __importStar(require('./server/download-server.js'))).then(m => {
91
93
  new m.DLServer(options);
92
94
  });
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,7 +267,8 @@ 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
  }
264
- (0, local_play_js_1.toLocalM3u8)(m3u8Info.data, options.filename);
270
+ result.stats = stats;
271
+ (0, local_play_js_1.toLocalM3u8)(m3u8Info.data);
265
272
  if (options.onInited)
266
273
  options.onInited(stats, m3u8Info, workPoll);
267
274
  runTask(m3u8Info.data);
@@ -30,7 +30,14 @@ async function parseM3U8(content, cacheDir = './cache', headers) {
30
30
  parser.end();
31
31
  utils_1.logger.debug('parser.manifest', parser.manifest);
32
32
  if (parser.manifest.playlists?.length > 0) {
33
- url = new URL(parser.manifest.playlists[0].uri, url).toString();
33
+ let maxBandwidthItem = parser.manifest.playlists[0];
34
+ for (const item of parser.manifest.playlists) {
35
+ if (!maxBandwidthItem || (item.attributes?.BANDWIDTH || 0) > (maxBandwidthItem.attributes?.BANDWIDTH || 0)) {
36
+ maxBandwidthItem = item;
37
+ }
38
+ }
39
+ url = new URL(maxBandwidthItem.uri, url).toString();
40
+ utils_1.logger.debug('maxBandwidthItem', maxBandwidthItem, url);
34
41
  content = (await (0, utils_1.getRetry)(url, headers)).data;
35
42
  parser = new m3u8_parser_1.Parser();
36
43
  parser.push(content);
@@ -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
  };
@@ -6,12 +6,15 @@ interface DLServerOptions {
6
6
  cacheDir?: string;
7
7
  configPath?: string;
8
8
  debug?: boolean;
9
- /** 登录 token,默认取环境变量 DS_TOKEN */
9
+ /** 登录 token,默认取环境变量 DS_SECRET */
10
10
  token?: string;
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;
@@ -53,7 +50,7 @@ class DLServer {
53
50
  options = {
54
51
  port: Number(process.env.DS_PORT) || 6600,
55
52
  cacheDir: (0, node_path_1.resolve)(process.cwd(), './cache'),
56
- token: process.env.DS_TOKEN || '',
53
+ token: process.env.DS_SECRET || process.env.DS_TOKEN || '',
57
54
  debug: process.env.DS_DEBUG == '1',
58
55
  };
59
56
  serverInfo = {
@@ -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
  /** 下载任务缓存 */
@@ -91,6 +89,8 @@ class DLServer {
91
89
  const pkg = JSON.parse((0, node_fs_1.readFileSync)(pkgFile, 'utf8'));
92
90
  this.serverInfo.version = pkg.version;
93
91
  }
92
+ if (opts.token)
93
+ opts.token = (0, fe_utils_1.md5)(opts.token.trim()).slice(0, 8);
94
94
  this.init();
95
95
  }
96
96
  async init() {
@@ -175,11 +175,10 @@ class DLServer {
175
175
  const { default: express } = await Promise.resolve().then(() => __importStar(require('express')));
176
176
  const { WebSocketServer } = await Promise.resolve().then(() => __importStar(require('ws')));
177
177
  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}`));
178
+ 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
179
  const wss = (this.wss = new WebSocketServer({ server }));
180
180
  app.use(express.json());
181
181
  app.use(express.static((0, node_path_1.resolve)(__dirname, '../../client')));
182
- // headers
183
182
  app.use((req, res, next) => {
184
183
  res.setHeader('Access-Control-Allow-Origin', '*');
185
184
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
@@ -213,28 +212,15 @@ class DLServer {
213
212
  }
214
213
  ws.send(JSON.stringify({ type: 'serverInfo', data: this.serverInfo }));
215
214
  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
215
  });
216
+ wss.on('close', () => utils_js_1.logger.info('WebSocket server closed'));
217
+ wss.on('error', err => utils_js_1.logger.error('WebSocket server error:', err));
218
+ wss.on('listening', () => utils_js_1.logger.info(`WebSocket server listening on port ${(0, console_log_colors_1.green)(this.options.port)}`));
229
219
  return { app, wss };
230
220
  }
231
221
  startDownload(url, options) {
232
222
  const cacheItem = this.dlCache.get(url);
233
- const dlOptions = {
234
- ...this.cfg.dlOptions,
235
- ...options,
236
- cacheDir: this.options.cacheDir,
237
- };
223
+ const dlOptions = (0, format_options_js_1.formatOptions)(url, { ...this.cfg.dlOptions, ...options, cacheDir: this.options.cacheDir })[1];
238
224
  utils_js_1.logger.debug('startDownload', url, dlOptions, cacheItem?.status);
239
225
  if (cacheItem && cacheItem?.status === 'resume')
240
226
  return cacheItem.options;
@@ -242,12 +228,11 @@ class DLServer {
242
228
  if (cacheItem)
243
229
  cacheItem.status = 'pending';
244
230
  else
245
- this.dlCache.set(url, { options: dlOptions, status: 'pending', url });
231
+ this.dlCache.set(url, { options, dlOptions, status: 'pending', url });
246
232
  return cacheItem?.options || dlOptions;
247
233
  }
248
234
  let workPoll = cacheItem?.workPoll;
249
- const useVideoParser = index_js_1.VideoParser.getPlatform(url) !== null;
250
- const defaultItem = { options: dlOptions, status: 'resume', url };
235
+ const defaultItem = { options, dlOptions, status: 'resume', url };
251
236
  const opts = {
252
237
  ...dlOptions,
253
238
  showProgress: dlOptions.debug || this.options.debug,
@@ -259,20 +244,21 @@ class DLServer {
259
244
  this.dlCache.set(url, item);
260
245
  this.saveCache();
261
246
  this.wsSend('progress', url);
247
+ return status !== 'pause';
262
248
  },
263
249
  };
264
- const afterDownload = ({ filepath = '', errmsg = '' }) => {
250
+ const afterDownload = (r, url) => {
265
251
  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;
252
+ if (r.filepath && (0, node_fs_1.existsSync)(r.filepath)) {
253
+ item.localVideo = r.filepath;
254
+ item.downloadedSize = (0, node_fs_1.statSync)(r.filepath).size;
269
255
  }
270
- else if (!errmsg && opts.convert !== false)
271
- errmsg = '下载失败';
256
+ else if (!r.errmsg && opts.convert !== false)
257
+ r.errmsg = '下载失败';
272
258
  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));
259
+ item.errmsg = r.errmsg;
260
+ item.status = r.errmsg ? 'error' : 'done';
261
+ 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
262
  this.dlCache.set(url, item);
277
263
  this.wsSend('progress', url);
278
264
  this.saveCache();
@@ -288,21 +274,19 @@ class DLServer {
288
274
  if (cacheItem)
289
275
  cacheItem.status = 'resume';
290
276
  try {
291
- if (useVideoParser) {
277
+ if (dlOptions.type === 'parser') {
292
278
  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
- });
279
+ vp.download(url, opts).then(r => afterDownload(r, url));
280
+ }
281
+ else if (dlOptions.type === 'file') {
282
+ (0, file_download_js_1.fileDownload)(url, opts).then(r => afterDownload(r, url));
296
283
  }
297
284
  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
- });
285
+ (0, m3u8_download_js_1.m3u8Download)(url, opts).then(r => afterDownload(r, url));
302
286
  }
303
287
  }
304
288
  catch (error) {
305
- afterDownload({ filepath: '', errmsg: error.message });
289
+ afterDownload({ filepath: '', errmsg: error.message }, url);
306
290
  utils_js_1.logger.error('下载失败:', error);
307
291
  }
308
292
  return dlOptions;
@@ -315,8 +299,10 @@ class DLServer {
315
299
  const item = this.dlCache.get(data);
316
300
  if (item) {
317
301
  const { workPoll, ...stats } = item;
318
- data = { ...stats, url: data };
302
+ data = [{ ...stats, url: data }];
319
303
  }
304
+ else
305
+ return;
320
306
  }
321
307
  // 广播进度信息给所有客户端
322
308
  this.wss.clients.forEach(client => {
@@ -330,13 +316,11 @@ class DLServer {
330
316
  app.get('/healthcheck', (_req, res) => {
331
317
  res.json({ message: 'ok', code: 0 });
332
318
  });
333
- // API to set default config
334
319
  app.post('/config', (req, res) => {
335
320
  const config = req.body;
336
321
  this.saveConfig(config);
337
322
  res.json({ message: 'Config updated successfully', code: 0 });
338
323
  });
339
- // API to get default config
340
324
  app.get('/config', (_req, res) => {
341
325
  res.json({ ...this.cfg.dlOptions, ...this.cfg.webOptions });
342
326
  });
@@ -406,7 +390,7 @@ class DLServer {
406
390
  const list = [];
407
391
  for (const url of urlsToPause) {
408
392
  const { workPoll, ...item } = this.dlCache.get(url);
409
- if (item?.status === 'resume') {
393
+ if (['resume', 'pending'].includes(item?.status)) {
410
394
  (0, m3u8_download_js_1.m3u8DLStop)(url, workPoll);
411
395
  item.status = item.tsSuccess === item.tsCount ? 'done' : 'pause';
412
396
  list.push(item);
@@ -422,18 +406,18 @@ class DLServer {
422
406
  const urlsToResume = all ? [...this.dlCache.keys()] : urls;
423
407
  const list = [];
424
408
  for (const url of urlsToResume) {
425
- const { workPoll, ...item } = this.dlCache.get(url);
409
+ const item = this.dlCache.get(url);
426
410
  if (['pause', 'error'].includes(item?.status)) {
427
411
  this.startDownload(url, item.options);
428
- list.push(item);
412
+ const { workPoll, ...t } = item;
413
+ list.push(t);
429
414
  }
430
415
  else
431
416
  console.log(item?.status, url);
432
417
  }
433
- const count = list.length;
434
- if (count)
418
+ if (list.length)
435
419
  this.wsSend('progress', list);
436
- res.json({ message: count ? `已恢复 ${count} 个下载任务` : '没有找到可恢复的下载任务', code: count ? 0 : 1 });
420
+ res.json({ message: list.length ? `已恢复 ${list.length} 个下载任务` : '没有找到可恢复的下载任务', code: 0, count: list.length });
437
421
  });
438
422
  // API to delete download
439
423
  app.post('/delete', (req, res) => {
@@ -460,10 +444,9 @@ class DLServer {
460
444
  }
461
445
  }
462
446
  }
463
- const count = list.length;
464
- if (count)
447
+ if (list.length)
465
448
  this.wsSend('delete', list);
466
- res.json({ message: `已删除 ${count} 个下载任务`, code: 0, count });
449
+ res.json({ message: `已删除 ${list.length} 个下载任务`, code: 0, count: list.length });
467
450
  });
468
451
  app.get(/^\/localplay\/(.*)$/, (req, res) => {
469
452
  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
@@ -9,9 +9,11 @@
9
9
  <title>M3U8 下载管理</title>
10
10
  <link rel="icon" type="image/svg+xml" href="logo.svg">
11
11
  <script src="https://cdn.tailwindcss.com"></script>
12
- <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
13
- <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
14
- <link href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.7.2/css/all.min.css" rel="stylesheet">
12
+ <script src="https://s4.zstatic.net/ajax/libs/vue/2.7.16/vue.min.js" integrity="sha512-Wx8niGbPNCD87mSuF0sBRytwW2+2ZFr7HwVDF8krCb3egstCc4oQfig+/cfg2OHd82KcUlOYxlSDAqdHqK5TCw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
13
+ <script src="https://s4.zstatic.net/ajax/libs/sweetalert2/11.16.1/sweetalert2.min.js" integrity="sha512-LGHBR+kJ5jZSIzhhdfytPoEHzgaYuTRifq9g5l6ja6/k9NAOsAi5dQh4zQF6JIRB8cAYxTRedERUF+97/KuivQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
14
+ <script src="https://s4.zstatic.net/ajax/libs/blueimp-md5/2.19.0/js/md5.min.js" integrity="sha512-8pbzenDolL1l5OPSsoURCx9TEdMFTaeFipASVrMYKhuYtly+k3tcsQYliOEKTmuB1t7yuzAiVo+yd7SJz+ijFQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
15
+ <link rel="stylesheet" href="https://s4.zstatic.net/ajax/libs/sweetalert2/11.16.1/sweetalert2.min.css" integrity="sha512-WnmDqbbAeHb7Put2nIAp7KNlnMup0FXVviOctducz1omuXB/hHK3s2vd3QLffK/CvvFUKrpioxdo+/Jo3k/xIw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
16
+ <link rel="stylesheet" href="https://s4.zstatic.net/ajax/libs/font-awesome/6.7.2/css/all.min.css" integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
15
17
 
16
18
  <style>
17
19
  #app {
@@ -139,7 +141,7 @@
139
141
  </nav>
140
142
  </div>
141
143
 
142
- <div class="main-content p-6"
144
+ <div class="main-content p-1 md:p-6"
143
145
  :style="{ marginLeft: sidebarCollapsed ? '0' : '16rem', width: sidebarCollapsed ? '100%' : 'calc(100% - 16rem)' }">
144
146
  <div v-if="activeSection === 'about'" class="bg-white rounded-lg shadow p-6">
145
147
  <h2 class="text-xl font-semibold mb-6">关于项目</h2>
@@ -426,9 +428,9 @@ services:
426
428
  <div class="flex items-center">
427
429
  <input type="checkbox" :checked="selectedTasks.includes(task.url)" @change="toggleTaskSelection(task.url)"
428
430
  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)">
430
- <h3 class="font-bold text-green-600 truncate" :title="task.url">
431
- {{ task.localVideo || task.filename || task.url }}
431
+ title="'选择任务:' + (task.localVideo || task.filename || task.url)">
432
+ <h3 class="font-bold text-green-600 truncate max-w-[calc(100vw-100px)]" :title="task.url">
433
+ {{ task.filename || task.localVideo || task.url }}
432
434
  </h3>
433
435
  <span v-if="task.status === 'pending'" class="ml-2 px-2 py-0.5 text-xs bg-yellow-100 text-yellow-800 rounded">等待中</span>
434
436
  <span v-else-if="task.status === 'resume'"
@@ -449,7 +451,7 @@ services:
449
451
  <i class="fas fa-clock mr-1"></i>
450
452
  <span>时长: {{ T.formatTime(task.duration * 1000) }}</span>
451
453
  </span><span class="flex items-center">
452
- <i class="fas fa-clock mr-1"></i>
454
+ <i class="fas fa-file-video mr-1"></i>
453
455
  <span>大小: {{ T.formatSize(task.size || task.downloadedSize) }}</span>
454
456
  </span>
455
457
  <span v-if="task.tsCount" class="flex items-center">
@@ -547,7 +549,8 @@ services:
547
549
  },
548
550
  alert(msg, p) {
549
551
  p = typeof msg === 'object' ? msg : Object.assign({ text: msg }, p);
550
- Swal.fire(Object.assign({ icon: 'info', showConfirmButton: false, showCloseButton: true, allowOutsideClick: false }, p));
552
+ if (!p.toast) p.allowOutsideClick = false;
553
+ Swal.fire(Object.assign({ icon: 'info', showConfirmButton: false, showCloseButton: true }, p));
551
554
  },
552
555
  toast(msg, p) {
553
556
  p = typeof msg === 'object' ? msg : Object.assign({ text: msg }, p);
@@ -601,7 +604,7 @@ services:
601
604
  };
602
605
 
603
606
  Vue.prototype.T = T;
604
- new Vue({
607
+ window.APP = new Vue({
605
608
  el: '#app',
606
609
  data: {
607
610
  ws: null,
@@ -691,9 +694,12 @@ services:
691
694
  forceUpdate: function () {
692
695
  const now = Date.now();
693
696
  if (now - this.forceUpdateTime > 500) {
694
- this.$forceUpdate();
695
697
  this.forceUpdateTime = now;
696
- } else this.forceUpdate();
698
+ this.$forceUpdate();
699
+ } else {
700
+ if (this.forceUpdateTimeout) clearTimeout(this.forceUpdateTimeout);
701
+ this.forceUpdateTimeout = setTimeout(() => this.forceUpdate(), 500);
702
+ }
697
703
  },
698
704
  wsConnect: function (reconnectDelay = 3000) {
699
705
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
@@ -716,11 +722,14 @@ services:
716
722
  if (!Array.isArray(data)) data = [data];
717
723
  this.$nextTick(() => {
718
724
  data.forEach(item => item.url && (this.tasks[item.url] = item));
719
- // this.forceUpdate();
725
+ this.forceUpdate();
720
726
  });
721
727
  break;
722
728
  case 'delete':
723
- if (Array.isArray(data)) data.forEach(url => delete this.tasks[url]);
729
+ if (Array.isArray(data)) {
730
+ data.forEach(url => delete this.tasks[url]);
731
+ this.forceUpdate();
732
+ }
724
733
  break;
725
734
  case 'queueStatus':
726
735
  this.queueStatus = data;
@@ -767,6 +776,8 @@ services:
767
776
  const isUpdated = this.token !== T.reqHeaders.authorization;
768
777
  if (!isUpdated) return;
769
778
 
779
+ if (this.token) this.token = md5(this.token).slice(0, 8);
780
+
770
781
  T.reqHeaders.authorization = this.token || '';
771
782
  if (this.token) {
772
783
  localStorage.setItem('token', this.token);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzwme/m3u8-dl",
3
- "version": "1.1.3",
3
+ "version": "1.2.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",
@@ -43,27 +43,28 @@
43
43
  "registry": "https://registry.npmjs.com"
44
44
  },
45
45
  "devDependencies": {
46
- "@eslint/js": "^9.27.0",
46
+ "@eslint/js": "^9.28.0",
47
47
  "@lzwme/fed-lint-helper": "^2.6.6",
48
48
  "@types/express": "^5.0.2",
49
- "@types/node": "^22.15.18",
49
+ "@types/m3u8-parser": "^7.2.2",
50
+ "@types/node": "^22.15.29",
50
51
  "@types/ws": "^8.18.1",
51
- "@typescript-eslint/eslint-plugin": "^8.32.1",
52
- "@typescript-eslint/parser": "^8.32.1",
53
- "eslint": "^9.27.0",
52
+ "@typescript-eslint/eslint-plugin": "^8.33.0",
53
+ "@typescript-eslint/parser": "^8.33.0",
54
+ "eslint": "^9.28.0",
54
55
  "eslint-config-prettier": "^10.1.5",
55
- "eslint-plugin-prettier": "^5.4.0",
56
+ "eslint-plugin-prettier": "^5.4.1",
56
57
  "express": "^5.1.0",
57
58
  "husky": "^9.1.7",
58
59
  "prettier": "^3.5.3",
59
60
  "standard-version": "^9.5.0",
60
61
  "typescript": "^5.8.3",
61
- "typescript-eslint": "^8.32.1",
62
+ "typescript-eslint": "^8.33.0",
62
63
  "ws": "^8.18.2"
63
64
  },
64
65
  "dependencies": {
65
66
  "@lzwme/fe-utils": "^1.9.0",
66
- "commander": "^13.1.0",
67
+ "commander": "^14.0.0",
67
68
  "console-log-colors": "^0.5.0",
68
69
  "enquirer": "^2.4.1",
69
70
  "m3u8-parser": "^7.2.0"