@lzwme/m3u8-dl 1.3.1 → 1.4.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/cjs/index.d.ts +1 -0
- package/cjs/index.js +1 -0
- package/cjs/lib/file-download.js +1 -1
- package/cjs/lib/format-options.d.ts +5 -1
- package/cjs/lib/format-options.js +1 -1
- package/cjs/lib/getM3u8Urls.d.ts +2 -0
- package/cjs/lib/getM3u8Urls.js +50 -0
- package/cjs/lib/m3u8-download.js +10 -10
- package/cjs/lib/worker_pool.js +4 -0
- package/cjs/m3u8-batch-download.js +2 -2
- package/cjs/server/download-server.js +34 -17
- package/client/index.html +60 -21
- package/client/logo.png +0 -0
- package/client/style.css +8 -0
- package/package.json +8 -8
- package/client/logo.svg +0 -16
package/cjs/index.d.ts
CHANGED
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);
|
package/cjs/lib/file-download.js
CHANGED
|
@@ -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
|
|
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):
|
|
2
|
+
export declare function formatOptions(url: string, opts: M3u8DLOptions): {
|
|
3
|
+
url: string;
|
|
4
|
+
options: M3u8DLOptions;
|
|
5
|
+
urlMd5: string;
|
|
6
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
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, deep = 2, visited = new Set()) {
|
|
8
|
+
const req = new fe_utils_1.ReqFetch({ headers: { 'content-type': 'text/html; charset=UTF-8' }, reqOptions: {} });
|
|
9
|
+
const { data: html } = await req.get(url);
|
|
10
|
+
// 从 html 中正则匹配提取 m3u8
|
|
11
|
+
const m3u8Urls = new Map();
|
|
12
|
+
const m3u8Regex = /https?:[^\s'"]+\.m3u8(\?[^\s'"]*)?/gi;
|
|
13
|
+
// 1. 直接正则匹配 m3u8 地址
|
|
14
|
+
let match = m3u8Regex.exec(html);
|
|
15
|
+
while (match) {
|
|
16
|
+
const title = (/<title>([^<]+)</.exec(html)?.[1].split('-')[0] || '').replace(/在线播放|《|》/g, '');
|
|
17
|
+
const href = match[0].replaceAll('\\/', '/');
|
|
18
|
+
if (!m3u8Urls.has(href))
|
|
19
|
+
m3u8Urls.set(href, title);
|
|
20
|
+
match = m3u8Regex.exec(html);
|
|
21
|
+
}
|
|
22
|
+
// 2. 若未找到且深度大于 0,则获取所有 a 标签的 href 并递归查找
|
|
23
|
+
if (m3u8Urls.size === 0 && deep > 0) {
|
|
24
|
+
utils_js_1.logger.debug('未获取到 m3u8 地址', fe_utils_1.color.gray(url), html.length);
|
|
25
|
+
visited.add(url);
|
|
26
|
+
const aTagRegex = /<a\s+[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi;
|
|
27
|
+
let aMatch = aTagRegex.exec(html);
|
|
28
|
+
const origin = new URL(url).origin;
|
|
29
|
+
while (aMatch) {
|
|
30
|
+
const href = aMatch[1] ? new URL(aMatch[1], origin).toString() : '';
|
|
31
|
+
const text = aMatch[2];
|
|
32
|
+
aMatch = aTagRegex.exec(html);
|
|
33
|
+
if (!href || visited.has(href) || !href.startsWith(origin) || !/集|HD|高清|播放/.test(text))
|
|
34
|
+
continue;
|
|
35
|
+
visited.add(href);
|
|
36
|
+
try {
|
|
37
|
+
const subUrls = await getM3u8Urls(href, deep - 1, visited);
|
|
38
|
+
utils_js_1.logger.debug(' > 从子页面提取: ', fe_utils_1.color.gray(href), text, subUrls.size);
|
|
39
|
+
for (const [u, t] of subUrls)
|
|
40
|
+
m3u8Urls.set(u, t || text);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
utils_js_1.logger.warn(' > 尝试访问子页面异常: ', fe_utils_1.color.red(href), err.message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return m3u8Urls;
|
|
48
|
+
}
|
|
49
|
+
// logger.updateOptions({ levelType: 'debug' });
|
|
50
|
+
// getM3u8Urls(process.argv.slice(2)[0]).then(d => console.log(d));
|
package/cjs/lib/m3u8-download.js
CHANGED
|
@@ -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(
|
|
90
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
}
|
package/cjs/lib/worker_pool.js
CHANGED
|
@@ -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
|
|
42
|
-
taskset.set(
|
|
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
|
}
|
|
@@ -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
|
|
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
|
}
|
|
@@ -249,7 +250,7 @@ class DLServer {
|
|
|
249
250
|
return { app, wss };
|
|
250
251
|
}
|
|
251
252
|
startDownload(url, options) {
|
|
252
|
-
const dlOptions = (0, format_options_js_1.formatOptions)(url, { ...this.cfg.dlOptions, ...options, cacheDir: this.options.cacheDir })
|
|
253
|
+
const { options: dlOptions } = (0, format_options_js_1.formatOptions)(url, { ...this.cfg.dlOptions, ...options, cacheDir: this.options.cacheDir });
|
|
253
254
|
const cacheItem = this.dlCache.get(url) || { options, dlOptions, status: 'pending', url };
|
|
254
255
|
utils_js_1.logger.debug('startDownload', url, dlOptions, cacheItem.status);
|
|
255
256
|
if (cacheItem.status === 'resume')
|
|
@@ -337,24 +338,23 @@ class DLServer {
|
|
|
337
338
|
}
|
|
338
339
|
initRouters() {
|
|
339
340
|
const { app } = this;
|
|
340
|
-
// health check
|
|
341
341
|
app.get('/healthcheck', (_req, res) => {
|
|
342
342
|
res.json({ message: 'ok', code: 0 });
|
|
343
343
|
});
|
|
344
|
-
app.post('/config', (req, res) => {
|
|
344
|
+
app.post('/api/config', (req, res) => {
|
|
345
345
|
const config = req.body;
|
|
346
346
|
this.saveConfig(config);
|
|
347
347
|
res.json({ message: 'Config updated successfully', code: 0 });
|
|
348
348
|
});
|
|
349
|
-
app.get('/config', (_req, res) => {
|
|
349
|
+
app.get('/api/config', (_req, res) => {
|
|
350
350
|
res.json({ ...this.cfg.dlOptions, ...this.cfg.webOptions });
|
|
351
351
|
});
|
|
352
352
|
// API to get all download progress
|
|
353
|
-
app.get('/tasks', (_req, res) => {
|
|
353
|
+
app.get('/api/tasks', (_req, res) => {
|
|
354
354
|
res.json(Object.fromEntries(this.dlCacheClone()));
|
|
355
355
|
});
|
|
356
356
|
// API to get queue status
|
|
357
|
-
app.get('/queue/status', (_req, res) => {
|
|
357
|
+
app.get('/api/queue/status', (_req, res) => {
|
|
358
358
|
const pendingTasks = Array.from(this.dlCache.entries()).filter(([_, item]) => item.status === 'pending');
|
|
359
359
|
const activeTasks = Array.from(this.dlCache.entries()).filter(([_, item]) => item.status === 'resume');
|
|
360
360
|
res.json({
|
|
@@ -364,7 +364,7 @@ class DLServer {
|
|
|
364
364
|
});
|
|
365
365
|
});
|
|
366
366
|
// API to clear queue
|
|
367
|
-
app.post('/queue/clear', (_req, res) => {
|
|
367
|
+
app.post('/api/queue/clear', (_req, res) => {
|
|
368
368
|
let count = 0;
|
|
369
369
|
for (const [url, item] of this.dlCache.entries()) {
|
|
370
370
|
if (item.status === 'pending') {
|
|
@@ -377,7 +377,7 @@ class DLServer {
|
|
|
377
377
|
res.json({ message: `已清空 ${count} 个等待中的下载任务`, code: 0 });
|
|
378
378
|
});
|
|
379
379
|
// API to update task priority
|
|
380
|
-
app.post('/priority', (req, res) => {
|
|
380
|
+
app.post('/api/priority', (req, res) => {
|
|
381
381
|
const { url, priority } = req.body;
|
|
382
382
|
const item = this.dlCache.get(url);
|
|
383
383
|
if (!item) {
|
|
@@ -389,7 +389,7 @@ class DLServer {
|
|
|
389
389
|
res.json({ message: '已更新任务优先级', code: 0 });
|
|
390
390
|
});
|
|
391
391
|
// API to start m3u8 download
|
|
392
|
-
app.post('/download', (req, res) => {
|
|
392
|
+
app.post('/api/download', (req, res) => {
|
|
393
393
|
const { url, options = {}, list = [] } = req.body;
|
|
394
394
|
try {
|
|
395
395
|
if (list.length) {
|
|
@@ -409,7 +409,7 @@ class DLServer {
|
|
|
409
409
|
}
|
|
410
410
|
});
|
|
411
411
|
// API to pause download
|
|
412
|
-
app.post('/pause', (req, res) => {
|
|
412
|
+
app.post('/api/pause', (req, res) => {
|
|
413
413
|
const { urls, all = false } = req.body;
|
|
414
414
|
const urlsToPause = all ? [...this.dlCache.keys()] : urls;
|
|
415
415
|
const list = [];
|
|
@@ -417,7 +417,7 @@ class DLServer {
|
|
|
417
417
|
const item = this.dlCache.get(url);
|
|
418
418
|
if (['resume', 'pending'].includes(item?.status)) {
|
|
419
419
|
(0, m3u8_download_js_1.m3u8DLStop)(url, item.workPoll);
|
|
420
|
-
item.status = item.tsSuccess === item.tsCount ? 'done' : 'pause';
|
|
420
|
+
item.status = item.tsSuccess > 0 && item.tsSuccess === item.tsCount ? 'done' : 'pause';
|
|
421
421
|
const { workPoll, ...tItem } = item;
|
|
422
422
|
list.push(tItem);
|
|
423
423
|
}
|
|
@@ -427,7 +427,7 @@ class DLServer {
|
|
|
427
427
|
res.json({ message: `已暂停 ${list.length} 个下载任务`, code: 0, count: list.length });
|
|
428
428
|
});
|
|
429
429
|
// API to resume download
|
|
430
|
-
app.post('/resume', (req, res) => {
|
|
430
|
+
app.post('/api/resume', (req, res) => {
|
|
431
431
|
const { urls, all = false } = req.body;
|
|
432
432
|
const urlsToResume = all ? [...this.dlCache.keys()] : urls;
|
|
433
433
|
const list = [];
|
|
@@ -446,7 +446,7 @@ class DLServer {
|
|
|
446
446
|
res.json({ message: list.length ? `已恢复 ${list.length} 个下载任务` : '没有找到可恢复的下载任务', code: 0, count: list.length });
|
|
447
447
|
});
|
|
448
448
|
// API to delete download
|
|
449
|
-
app.post('/delete', (req, res) => {
|
|
449
|
+
app.post('/api/delete', (req, res) => {
|
|
450
450
|
const { urls, deleteCache = false, deleteVideo = false } = req.body;
|
|
451
451
|
const urlsToDelete = urls;
|
|
452
452
|
const list = [];
|
|
@@ -458,20 +458,26 @@ class DLServer {
|
|
|
458
458
|
list.push(item.url);
|
|
459
459
|
if (deleteCache && item.current?.tsOut) {
|
|
460
460
|
const cacheDir = (0, node_path_1.dirname)(item.current.tsOut);
|
|
461
|
-
if ((0, node_fs_1.existsSync)(cacheDir))
|
|
461
|
+
if ((0, node_fs_1.existsSync)(cacheDir)) {
|
|
462
462
|
(0, node_fs_1.rmSync)(cacheDir, { recursive: true });
|
|
463
|
+
utils_js_1.logger.debug('删除缓存目录:', cacheDir);
|
|
464
|
+
}
|
|
463
465
|
}
|
|
464
466
|
if (deleteVideo) {
|
|
465
467
|
['.ts', '.mp4'].forEach(ext => {
|
|
466
468
|
const filepath = (0, node_path_1.resolve)(item.options.saveDir, item.options.filename + ext);
|
|
467
|
-
if ((0, node_fs_1.existsSync)(filepath))
|
|
469
|
+
if ((0, node_fs_1.existsSync)(filepath)) {
|
|
468
470
|
(0, node_fs_1.unlinkSync)(filepath);
|
|
471
|
+
utils_js_1.logger.debug('删除文件:', filepath);
|
|
472
|
+
}
|
|
469
473
|
});
|
|
470
474
|
}
|
|
471
475
|
}
|
|
472
476
|
}
|
|
473
|
-
if (list.length)
|
|
477
|
+
if (list.length) {
|
|
474
478
|
this.wsSend('delete', list);
|
|
479
|
+
this.saveCache();
|
|
480
|
+
}
|
|
475
481
|
res.json({ message: `已删除 ${list.length} 个下载任务`, code: 0, count: list.length });
|
|
476
482
|
});
|
|
477
483
|
app.get(/^\/localplay\/(.*)$/, (req, res) => {
|
|
@@ -519,6 +525,17 @@ class DLServer {
|
|
|
519
525
|
utils_js_1.logger.error('Localplay file not found:', filepath);
|
|
520
526
|
res.status(404).send({ message: 'Not Found', code: 404 });
|
|
521
527
|
});
|
|
528
|
+
app.post('/api/getM3u8Urls', (req, res) => {
|
|
529
|
+
const url = req.body.url;
|
|
530
|
+
if (!url) {
|
|
531
|
+
res.json({ code: 1001, message: '无效的 url 参数' });
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
(0, getM3u8Urls_js_1.getM3u8Urls)(url)
|
|
535
|
+
.then(d => res.json({ code: 0, data: Array.from(d) }))
|
|
536
|
+
.catch(err => res.json({ code: 401, message: err.message }));
|
|
537
|
+
}
|
|
538
|
+
});
|
|
522
539
|
}
|
|
523
540
|
}
|
|
524
541
|
exports.DLServer = DLServer;
|
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/
|
|
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.
|
|
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"
|
|
@@ -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: '取消' });
|
|
@@ -790,7 +790,7 @@ services:
|
|
|
790
790
|
},
|
|
791
791
|
/** 获取配置 */
|
|
792
792
|
fetchConfig: async function () {
|
|
793
|
-
const config = await T.get('/config');
|
|
793
|
+
const config = await T.get('/api/config');
|
|
794
794
|
if (config.code) {
|
|
795
795
|
console.error('获取配置失败:', config);
|
|
796
796
|
T.alert('获取配置失败: ' + config.message, { icon: 'error' });
|
|
@@ -803,7 +803,7 @@ services:
|
|
|
803
803
|
},
|
|
804
804
|
/** 更新配置 */
|
|
805
805
|
updateConfig: async function () {
|
|
806
|
-
const result = await T.post('/config', this.config);
|
|
806
|
+
const result = await T.post('/api/config', this.config);
|
|
807
807
|
T.toast(result.message || '配置已更新', { icon: result.code ? 'error' : 'success' });
|
|
808
808
|
},
|
|
809
809
|
updateLocalConfig: async function () {
|
|
@@ -830,18 +830,31 @@ services:
|
|
|
830
830
|
width: '800px',
|
|
831
831
|
html: `
|
|
832
832
|
<div class="text-left">
|
|
833
|
-
<
|
|
834
|
-
|
|
833
|
+
<div class="flex flex-row gap-4">
|
|
834
|
+
<input type="text" id="playUrl" placeholder="输入视频播放页地址,尝试提取m3u8下载链接" autocomplete="off" id="urlInput" class="flex-1 border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400" value="">
|
|
835
|
+
<div class="flex flex-row gap-1">
|
|
836
|
+
<button type="button" id="getM3u8UrlsBtn" class="player-btn px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none">
|
|
837
|
+
提取
|
|
838
|
+
</button>
|
|
839
|
+
</div>
|
|
840
|
+
</div>
|
|
835
841
|
|
|
836
842
|
<div class="mt-4">
|
|
837
|
-
<label class="block text-sm font-bold text-gray-700 mb-1"
|
|
838
|
-
<
|
|
839
|
-
<p class="mt-1 text-sm text-gray-500">若输入多个链接,将依次以"视频名称+第N集"命名</p>
|
|
843
|
+
<label class="block text-sm font-bold text-gray-700 mb-1">视频链接(每行一个,支持m3u8地址及抖音、微博、皮皮虾视频分享链接)</label>
|
|
844
|
+
<textarea id="downloadUrls" class="w-full p-2 border rounded-lg focus:ring-blue-500" rows="3" placeholder="格式: URL | 名称(可选)、名称 | URL"></textarea>
|
|
840
845
|
</div>
|
|
841
846
|
|
|
842
847
|
<div class="mt-4">
|
|
848
|
+
<div class="flex flex-row gap-2 items-center">
|
|
849
|
+
<label class="block text-sm font-bold text-gray-700 mb-1">视频名称</label>
|
|
850
|
+
<input id="filename" class="flex-1 p-2 border rounded-lg focus:border-blue-500" placeholder="请输入视频名称(可选)">
|
|
851
|
+
</div>
|
|
852
|
+
<p class="ml-2 mt-1 text-sm text-gray-500">若输入多个链接,将依次以"视频名称+第N集"命名</p>
|
|
853
|
+
</div>
|
|
854
|
+
|
|
855
|
+
<div class="mt-4 flex flex-row gap-2 items-center">
|
|
843
856
|
<label class="block text-sm font-bold text-gray-700 mb-1">保存位置</label>
|
|
844
|
-
<input id="saveDir" class="
|
|
857
|
+
<input id="saveDir" class="flex-1 p-2 border rounded-lg focus:ring-blue-500" placeholder="请输入保存路径" value="${this.config.saveDir || ''}">
|
|
845
858
|
</div>
|
|
846
859
|
|
|
847
860
|
<div class="mt-4">
|
|
@@ -851,7 +864,7 @@ services:
|
|
|
851
864
|
|
|
852
865
|
<div class="mt-4">
|
|
853
866
|
<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="
|
|
867
|
+
<textarea id="headers" class="w-full p-2 border rounded-lg focus:ring-blue-500" rows="3" placeholder="每行一个(微博视频须设置 Cookie),格式:Key: Value。例如: Referer: https://example.com Cookie: token=123"></textarea>
|
|
855
868
|
</div>
|
|
856
869
|
</div>
|
|
857
870
|
`,
|
|
@@ -897,6 +910,31 @@ services:
|
|
|
897
910
|
}).then((result) => {
|
|
898
911
|
if (result.isConfirmed) this.startBatchDownload(result.value);
|
|
899
912
|
});
|
|
913
|
+
|
|
914
|
+
setTimeout(() => {
|
|
915
|
+
const btn = document.getElementById('getM3u8UrlsBtn');
|
|
916
|
+
if (!btn) return;
|
|
917
|
+
|
|
918
|
+
btn.addEventListener('click', async () => {
|
|
919
|
+
const url = document.getElementById('playUrl').value.trim();
|
|
920
|
+
if (!/^https?:/.test(url)) {
|
|
921
|
+
return Swal.showValidationMessage('请输入正确的 URL 地址');
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
btn.setAttribute('disabled', 'disabled');
|
|
925
|
+
btn.innerText = '解析中...';
|
|
926
|
+
|
|
927
|
+
T.post('/api/getM3u8Urls', { url }).then(r => {
|
|
928
|
+
if (Array.isArray(r.data)) {
|
|
929
|
+
document.getElementById('downloadUrls').value = r.data.map(d => d.join(' | ')).join('\n');
|
|
930
|
+
T.toast(r.message || `解析完成!获取到 ${r.data.length} 个地址`);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
btn.removeAttribute('disabled');
|
|
934
|
+
btn.innerText = '提取';
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
}, 500);
|
|
900
938
|
},
|
|
901
939
|
/** 批量下载 */
|
|
902
940
|
startBatchDownload: async function (list) {
|
|
@@ -905,7 +943,7 @@ services:
|
|
|
905
943
|
Object.entries(item).forEach(([key, value]) => !value && delete item[key]);
|
|
906
944
|
this.tasks[item.url] = { status: 'resume', progress: 0, speed: 0, remainingTime: 0, size: 0 };
|
|
907
945
|
});
|
|
908
|
-
const r = await T.post('/download', { list });
|
|
946
|
+
const r = await T.post('/api/download', { list });
|
|
909
947
|
if (!r.code) T.toast(r.message || '批量下载已开始');
|
|
910
948
|
this.forceUpdate();
|
|
911
949
|
} catch (error) {
|
|
@@ -917,7 +955,7 @@ services:
|
|
|
917
955
|
pauseDownload: async function (urls) {
|
|
918
956
|
if (!urls) urls = this.selectedTasks;
|
|
919
957
|
if (typeof urls === 'string') urls = [urls];
|
|
920
|
-
const r = await T.post(
|
|
958
|
+
const r = await T.post('/api/pause', { urls, all: urls[0] === 'all' });
|
|
921
959
|
if (!r.code) T.toast(r.message || '已暂停下载');
|
|
922
960
|
if (urls === this.selectedTasks) this.selectedTasks = [];
|
|
923
961
|
},
|
|
@@ -925,7 +963,7 @@ services:
|
|
|
925
963
|
resumeDownload: async function (urls) {
|
|
926
964
|
if (!urls) urls = this.selectedTasks;
|
|
927
965
|
if (typeof urls === 'string') urls = [urls];
|
|
928
|
-
const r = await T.post(
|
|
966
|
+
const r = await T.post('/api/resume', { urls, all: urls[0] === 'all' });
|
|
929
967
|
if (!r.code) T.toast(r.message || '已恢复下载');
|
|
930
968
|
if (urls === this.selectedTasks) this.selectedTasks = [];
|
|
931
969
|
},
|
|
@@ -965,7 +1003,7 @@ services:
|
|
|
965
1003
|
});
|
|
966
1004
|
|
|
967
1005
|
if (result.isConfirmed) {
|
|
968
|
-
const r = await T.post(
|
|
1006
|
+
const r = await T.post('/api/delete', {
|
|
969
1007
|
urls,
|
|
970
1008
|
deleteCache: result.value.deleteCache,
|
|
971
1009
|
deleteVideo: result.value.deleteVideo
|
|
@@ -984,7 +1022,7 @@ services:
|
|
|
984
1022
|
}
|
|
985
1023
|
},
|
|
986
1024
|
getTasks: async function () {
|
|
987
|
-
this.tasks = await T.get('/tasks');
|
|
1025
|
+
this.tasks = await T.get('/api/tasks');
|
|
988
1026
|
},
|
|
989
1027
|
showTaskDetail(task) {
|
|
990
1028
|
console.log(task);
|
|
@@ -1003,9 +1041,10 @@ services:
|
|
|
1003
1041
|
预估还需: !task.endTime && task.remainingTime && T.formatTimeCost(task.remainingTime),
|
|
1004
1042
|
相关信息: task.errmsg && `<span class=text-red-600>${task.errmsg}</span>`
|
|
1005
1043
|
};
|
|
1006
|
-
|
|
1044
|
+
T.alert({
|
|
1007
1045
|
title: '任务详情',
|
|
1008
1046
|
width: 1000,
|
|
1047
|
+
icon: '',
|
|
1009
1048
|
html: [
|
|
1010
1049
|
'<div class="flex-col full-width text-left">',
|
|
1011
1050
|
Object.entries(taskInfo).filter(d => d[1]).map(
|
|
@@ -1054,12 +1093,12 @@ services:
|
|
|
1054
1093
|
},
|
|
1055
1094
|
// 更新任务优先级
|
|
1056
1095
|
updatePriority: async function (url, priority) {
|
|
1057
|
-
const r = await T.post('/priority', { url, priority: parseInt(priority) });
|
|
1096
|
+
const r = await T.post('/api/priority', { url, priority: parseInt(priority) });
|
|
1058
1097
|
T.toast(r.message || '已更新优先级');
|
|
1059
1098
|
},
|
|
1060
1099
|
// 获取队列状态
|
|
1061
1100
|
getQueueStatus: async function () {
|
|
1062
|
-
const status = await T.get('/queue/status');
|
|
1101
|
+
const status = await T.get('/api/queue/status');
|
|
1063
1102
|
if (status?.maxConcurrent) this.queueStatus = status;
|
|
1064
1103
|
},
|
|
1065
1104
|
// 清空下载队列
|
|
@@ -1075,7 +1114,7 @@ services:
|
|
|
1075
1114
|
});
|
|
1076
1115
|
|
|
1077
1116
|
if (result.isConfirmed) {
|
|
1078
|
-
const r = await T.post('/queue/clear');
|
|
1117
|
+
const r = await T.post('/api/queue/clear');
|
|
1079
1118
|
T.toast(r.message || '已清空下载队列');
|
|
1080
1119
|
this.getQueueStatus();
|
|
1081
1120
|
}
|
package/client/logo.png
ADDED
|
Binary file
|
package/client/style.css
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lzwme/m3u8-dl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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",
|
|
@@ -44,24 +44,24 @@
|
|
|
44
44
|
"registry": "https://registry.npmjs.com"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@biomejs/biome": "^2.0.
|
|
47
|
+
"@biomejs/biome": "^2.0.6",
|
|
48
48
|
"@eslint/js": "^9.29.0",
|
|
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.
|
|
52
|
+
"@types/node": "^24.0.4",
|
|
53
53
|
"@types/ws": "^8.18.1",
|
|
54
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
55
|
-
"@typescript-eslint/parser": "^8.
|
|
54
|
+
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
|
55
|
+
"@typescript-eslint/parser": "^8.35.0",
|
|
56
56
|
"eslint": "^9.29.0",
|
|
57
57
|
"eslint-config-prettier": "^10.1.5",
|
|
58
|
-
"eslint-plugin-prettier": "^5.5.
|
|
58
|
+
"eslint-plugin-prettier": "^5.5.1",
|
|
59
59
|
"express": "^5.1.0",
|
|
60
60
|
"husky": "^9.1.7",
|
|
61
|
-
"prettier": "^3.
|
|
61
|
+
"prettier": "^3.6.2",
|
|
62
62
|
"standard-version": "^9.5.0",
|
|
63
63
|
"typescript": "^5.8.3",
|
|
64
|
-
"typescript-eslint": "^8.
|
|
64
|
+
"typescript-eslint": "^8.35.0",
|
|
65
65
|
"ws": "^8.18.2"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
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>
|