@lzwme/m3u8-dl 1.3.0 → 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/cli.js +3 -1
- 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.d.ts +2 -0
- package/cjs/server/download-server.js +58 -39
- package/client/index.html +109 -24
- package/client/logo.png +0 -0
- package/client/style.css +8 -0
- package/package.json +10 -10
- package/client/logo.svg +0 -16
package/cjs/cli.js
CHANGED
|
@@ -89,7 +89,9 @@ commander_1.program
|
|
|
89
89
|
const opts = getOptions();
|
|
90
90
|
if (opts.debug)
|
|
91
91
|
options.debug = true;
|
|
92
|
-
|
|
92
|
+
if (opts.cacheDir)
|
|
93
|
+
options.cacheDir = opts.cacheDir;
|
|
94
|
+
utils_js_1.logger.debug('[cli][server]', opts, options);
|
|
93
95
|
Promise.resolve().then(() => __importStar(require('./server/download-server.js'))).then(m => {
|
|
94
96
|
new m.DLServer(options);
|
|
95
97
|
});
|
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
|
}
|
|
@@ -36,11 +36,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.DLServer = void 0;
|
|
37
37
|
const node_fs_1 = require("node:fs");
|
|
38
38
|
const node_path_1 = require("node:path");
|
|
39
|
+
const node_os_1 = require("node:os");
|
|
39
40
|
const fe_utils_1 = require("@lzwme/fe-utils");
|
|
40
41
|
const console_log_colors_1 = require("console-log-colors");
|
|
41
42
|
const file_download_js_1 = require("../lib/file-download.js");
|
|
42
43
|
const format_options_js_1 = require("../lib/format-options.js");
|
|
43
44
|
const m3u8_download_js_1 = require("../lib/m3u8-download.js");
|
|
45
|
+
const getM3u8Urls_js_1 = require("../lib/getM3u8Urls.js");
|
|
44
46
|
const utils_js_1 = require("../lib/utils.js");
|
|
45
47
|
const index_js_1 = require("../video-parser/index.js");
|
|
46
48
|
const rootDir = (0, node_path_1.resolve)(__dirname, '../..');
|
|
@@ -50,9 +52,10 @@ class DLServer {
|
|
|
50
52
|
/** DS 参数 */
|
|
51
53
|
options = {
|
|
52
54
|
port: Number(process.env.DS_PORT) || 6600,
|
|
53
|
-
cacheDir: (0, node_path_1.resolve)(
|
|
55
|
+
cacheDir: process.env.DS_CACHE_DIR || (0, node_path_1.resolve)((0, node_os_1.homedir)(), '.m3u8-dl/cache'),
|
|
54
56
|
token: process.env.DS_SECRET || process.env.DS_TOKEN || '',
|
|
55
57
|
debug: process.env.DS_DEBUG === '1',
|
|
58
|
+
limitFileAccess: !['0', 'false'].includes(process.env.DS_LIMTE_FILE_ACCESS),
|
|
56
59
|
};
|
|
57
60
|
serverInfo = {
|
|
58
61
|
version: '',
|
|
@@ -102,7 +105,7 @@ class DLServer {
|
|
|
102
105
|
this.loadCache();
|
|
103
106
|
await this.createApp();
|
|
104
107
|
this.initRouters();
|
|
105
|
-
utils_js_1.logger.debug('Server initialized', this.options, this.cfg.dlOptions);
|
|
108
|
+
utils_js_1.logger.debug('Server initialized', 'cacheSize:', this.dlCache.size, this.options, this.cfg.dlOptions);
|
|
106
109
|
}
|
|
107
110
|
loadCache() {
|
|
108
111
|
const cacheFile = (0, node_path_1.resolve)(this.options.cacheDir, 'cache.json');
|
|
@@ -126,7 +129,7 @@ class DLServer {
|
|
|
126
129
|
return;
|
|
127
130
|
}
|
|
128
131
|
this.dlCache.forEach(item => {
|
|
129
|
-
if (item.status === 'done' && item.localVideo
|
|
132
|
+
if (item.status === 'done' && (!item.localVideo || !(0, node_fs_1.existsSync)(item.localVideo))) {
|
|
130
133
|
item.status = 'error';
|
|
131
134
|
item.errmsg = '已删除';
|
|
132
135
|
}
|
|
@@ -195,6 +198,7 @@ class DLServer {
|
|
|
195
198
|
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(rootDir, 'client/local/cdn'))) {
|
|
196
199
|
indexHtml = indexHtml
|
|
197
200
|
.replaceAll('https://s4.zstatic.net/ajax/libs', 'local/cdn')
|
|
201
|
+
.replaceAll(/integrity=.+\n/g, '')
|
|
198
202
|
.replace('https://cdn.tailwindcss.com/3.4.16', 'local/cdn/tailwindcss/3.4.16/tailwindcss.min.js');
|
|
199
203
|
}
|
|
200
204
|
res.setHeader('content-type', 'text/html').send(indexHtml);
|
|
@@ -246,18 +250,18 @@ class DLServer {
|
|
|
246
250
|
return { app, wss };
|
|
247
251
|
}
|
|
248
252
|
startDownload(url, options) {
|
|
249
|
-
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 });
|
|
250
254
|
const cacheItem = this.dlCache.get(url) || { options, dlOptions, status: 'pending', url };
|
|
251
|
-
utils_js_1.logger.debug('startDownload', url, dlOptions, cacheItem
|
|
255
|
+
utils_js_1.logger.debug('startDownload', url, dlOptions, cacheItem.status);
|
|
252
256
|
if (cacheItem.status === 'resume')
|
|
253
257
|
return cacheItem.options;
|
|
254
|
-
if (
|
|
255
|
-
cacheItem.
|
|
256
|
-
|
|
257
|
-
return cacheItem.options;
|
|
258
|
-
}
|
|
259
|
-
cacheItem.status = 'resume';
|
|
258
|
+
if (cacheItem.localVideo && !(0, node_fs_1.existsSync)(cacheItem.localVideo))
|
|
259
|
+
delete cacheItem.localVideo;
|
|
260
|
+
cacheItem.status = this.downloading >= this.cfg.webOptions.maxDownloads ? 'pending' : 'resume';
|
|
260
261
|
this.dlCache.set(url, cacheItem);
|
|
262
|
+
this.wsSend('progress', url);
|
|
263
|
+
if (cacheItem.status === 'pending')
|
|
264
|
+
return cacheItem.options;
|
|
261
265
|
let workPoll = cacheItem.workPoll;
|
|
262
266
|
const opts = {
|
|
263
267
|
...dlOptions,
|
|
@@ -291,12 +295,10 @@ class DLServer {
|
|
|
291
295
|
this.wsSend('progress', url);
|
|
292
296
|
this.saveCache();
|
|
293
297
|
// 找到一个 pending 的任务,开始下载
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
298
|
+
const nextItem = this.dlCache.entries().find(([_url, d]) => d.status === 'pending');
|
|
299
|
+
if (nextItem) {
|
|
300
|
+
this.startDownload(nextItem[0], nextItem[1].options);
|
|
301
|
+
this.wsSend('progress', nextItem[0]);
|
|
300
302
|
}
|
|
301
303
|
};
|
|
302
304
|
try {
|
|
@@ -323,12 +325,10 @@ class DLServer {
|
|
|
323
325
|
}
|
|
324
326
|
else if (type === 'progress' && typeof data === 'string') {
|
|
325
327
|
const item = this.dlCache.get(data);
|
|
326
|
-
if (item)
|
|
327
|
-
const { workPoll, ...stats } = item;
|
|
328
|
-
data = [{ ...stats, url: data }];
|
|
329
|
-
}
|
|
330
|
-
else
|
|
328
|
+
if (!item)
|
|
331
329
|
return;
|
|
330
|
+
const { workPoll, ...stats } = item;
|
|
331
|
+
data = [{ ...stats, url: data }];
|
|
332
332
|
}
|
|
333
333
|
// 广播进度信息给所有客户端
|
|
334
334
|
this.wss.clients.forEach(client => {
|
|
@@ -338,24 +338,23 @@ class DLServer {
|
|
|
338
338
|
}
|
|
339
339
|
initRouters() {
|
|
340
340
|
const { app } = this;
|
|
341
|
-
// health check
|
|
342
341
|
app.get('/healthcheck', (_req, res) => {
|
|
343
342
|
res.json({ message: 'ok', code: 0 });
|
|
344
343
|
});
|
|
345
|
-
app.post('/config', (req, res) => {
|
|
344
|
+
app.post('/api/config', (req, res) => {
|
|
346
345
|
const config = req.body;
|
|
347
346
|
this.saveConfig(config);
|
|
348
347
|
res.json({ message: 'Config updated successfully', code: 0 });
|
|
349
348
|
});
|
|
350
|
-
app.get('/config', (_req, res) => {
|
|
349
|
+
app.get('/api/config', (_req, res) => {
|
|
351
350
|
res.json({ ...this.cfg.dlOptions, ...this.cfg.webOptions });
|
|
352
351
|
});
|
|
353
352
|
// API to get all download progress
|
|
354
|
-
app.get('/tasks', (_req, res) => {
|
|
353
|
+
app.get('/api/tasks', (_req, res) => {
|
|
355
354
|
res.json(Object.fromEntries(this.dlCacheClone()));
|
|
356
355
|
});
|
|
357
356
|
// API to get queue status
|
|
358
|
-
app.get('/queue/status', (_req, res) => {
|
|
357
|
+
app.get('/api/queue/status', (_req, res) => {
|
|
359
358
|
const pendingTasks = Array.from(this.dlCache.entries()).filter(([_, item]) => item.status === 'pending');
|
|
360
359
|
const activeTasks = Array.from(this.dlCache.entries()).filter(([_, item]) => item.status === 'resume');
|
|
361
360
|
res.json({
|
|
@@ -365,7 +364,7 @@ class DLServer {
|
|
|
365
364
|
});
|
|
366
365
|
});
|
|
367
366
|
// API to clear queue
|
|
368
|
-
app.post('/queue/clear', (_req, res) => {
|
|
367
|
+
app.post('/api/queue/clear', (_req, res) => {
|
|
369
368
|
let count = 0;
|
|
370
369
|
for (const [url, item] of this.dlCache.entries()) {
|
|
371
370
|
if (item.status === 'pending') {
|
|
@@ -378,7 +377,7 @@ class DLServer {
|
|
|
378
377
|
res.json({ message: `已清空 ${count} 个等待中的下载任务`, code: 0 });
|
|
379
378
|
});
|
|
380
379
|
// API to update task priority
|
|
381
|
-
app.post('/priority', (req, res) => {
|
|
380
|
+
app.post('/api/priority', (req, res) => {
|
|
382
381
|
const { url, priority } = req.body;
|
|
383
382
|
const item = this.dlCache.get(url);
|
|
384
383
|
if (!item) {
|
|
@@ -390,7 +389,7 @@ class DLServer {
|
|
|
390
389
|
res.json({ message: '已更新任务优先级', code: 0 });
|
|
391
390
|
});
|
|
392
391
|
// API to start m3u8 download
|
|
393
|
-
app.post('/download', (req, res) => {
|
|
392
|
+
app.post('/api/download', (req, res) => {
|
|
394
393
|
const { url, options = {}, list = [] } = req.body;
|
|
395
394
|
try {
|
|
396
395
|
if (list.length) {
|
|
@@ -410,7 +409,7 @@ class DLServer {
|
|
|
410
409
|
}
|
|
411
410
|
});
|
|
412
411
|
// API to pause download
|
|
413
|
-
app.post('/pause', (req, res) => {
|
|
412
|
+
app.post('/api/pause', (req, res) => {
|
|
414
413
|
const { urls, all = false } = req.body;
|
|
415
414
|
const urlsToPause = all ? [...this.dlCache.keys()] : urls;
|
|
416
415
|
const list = [];
|
|
@@ -418,7 +417,7 @@ class DLServer {
|
|
|
418
417
|
const item = this.dlCache.get(url);
|
|
419
418
|
if (['resume', 'pending'].includes(item?.status)) {
|
|
420
419
|
(0, m3u8_download_js_1.m3u8DLStop)(url, item.workPoll);
|
|
421
|
-
item.status = item.tsSuccess === item.tsCount ? 'done' : 'pause';
|
|
420
|
+
item.status = item.tsSuccess > 0 && item.tsSuccess === item.tsCount ? 'done' : 'pause';
|
|
422
421
|
const { workPoll, ...tItem } = item;
|
|
423
422
|
list.push(tItem);
|
|
424
423
|
}
|
|
@@ -428,7 +427,7 @@ class DLServer {
|
|
|
428
427
|
res.json({ message: `已暂停 ${list.length} 个下载任务`, code: 0, count: list.length });
|
|
429
428
|
});
|
|
430
429
|
// API to resume download
|
|
431
|
-
app.post('/resume', (req, res) => {
|
|
430
|
+
app.post('/api/resume', (req, res) => {
|
|
432
431
|
const { urls, all = false } = req.body;
|
|
433
432
|
const urlsToResume = all ? [...this.dlCache.keys()] : urls;
|
|
434
433
|
const list = [];
|
|
@@ -447,7 +446,7 @@ class DLServer {
|
|
|
447
446
|
res.json({ message: list.length ? `已恢复 ${list.length} 个下载任务` : '没有找到可恢复的下载任务', code: 0, count: list.length });
|
|
448
447
|
});
|
|
449
448
|
// API to delete download
|
|
450
|
-
app.post('/delete', (req, res) => {
|
|
449
|
+
app.post('/api/delete', (req, res) => {
|
|
451
450
|
const { urls, deleteCache = false, deleteVideo = false } = req.body;
|
|
452
451
|
const urlsToDelete = urls;
|
|
453
452
|
const list = [];
|
|
@@ -459,20 +458,26 @@ class DLServer {
|
|
|
459
458
|
list.push(item.url);
|
|
460
459
|
if (deleteCache && item.current?.tsOut) {
|
|
461
460
|
const cacheDir = (0, node_path_1.dirname)(item.current.tsOut);
|
|
462
|
-
if ((0, node_fs_1.existsSync)(cacheDir))
|
|
461
|
+
if ((0, node_fs_1.existsSync)(cacheDir)) {
|
|
463
462
|
(0, node_fs_1.rmSync)(cacheDir, { recursive: true });
|
|
463
|
+
utils_js_1.logger.debug('删除缓存目录:', cacheDir);
|
|
464
|
+
}
|
|
464
465
|
}
|
|
465
466
|
if (deleteVideo) {
|
|
466
467
|
['.ts', '.mp4'].forEach(ext => {
|
|
467
468
|
const filepath = (0, node_path_1.resolve)(item.options.saveDir, item.options.filename + ext);
|
|
468
|
-
if ((0, node_fs_1.existsSync)(filepath))
|
|
469
|
+
if ((0, node_fs_1.existsSync)(filepath)) {
|
|
469
470
|
(0, node_fs_1.unlinkSync)(filepath);
|
|
471
|
+
utils_js_1.logger.debug('删除文件:', filepath);
|
|
472
|
+
}
|
|
470
473
|
});
|
|
471
474
|
}
|
|
472
475
|
}
|
|
473
476
|
}
|
|
474
|
-
if (list.length)
|
|
477
|
+
if (list.length) {
|
|
475
478
|
this.wsSend('delete', list);
|
|
479
|
+
this.saveCache();
|
|
480
|
+
}
|
|
476
481
|
res.json({ message: `已删除 ${list.length} 个下载任务`, code: 0, count: list.length });
|
|
477
482
|
});
|
|
478
483
|
app.get(/^\/localplay\/(.*)$/, (req, res) => {
|
|
@@ -484,8 +489,9 @@ class DLServer {
|
|
|
484
489
|
if (!(0, node_fs_1.existsSync)(filepath))
|
|
485
490
|
filepath += '.m3u8';
|
|
486
491
|
}
|
|
492
|
+
const allowedDirs = [this.options.cacheDir, this.cfg.dlOptions.saveDir];
|
|
487
493
|
if (!(0, node_fs_1.existsSync)(filepath)) {
|
|
488
|
-
for (const dir of
|
|
494
|
+
for (const dir of allowedDirs) {
|
|
489
495
|
const tpath = (0, node_path_1.resolve)(dir, filepath);
|
|
490
496
|
if ((0, node_fs_1.existsSync)(tpath)) {
|
|
491
497
|
filepath = tpath;
|
|
@@ -495,7 +501,9 @@ class DLServer {
|
|
|
495
501
|
}
|
|
496
502
|
else {
|
|
497
503
|
filepath = (0, node_path_1.resolve)(filepath);
|
|
498
|
-
|
|
504
|
+
const isAllow = !this.options.limitFileAccess || allowedDirs.some(d => filepath.startsWith((0, node_path_1.resolve)(d)));
|
|
505
|
+
if (!isAllow) {
|
|
506
|
+
utils_js_1.logger.error('[Localplay] Access denied:', filepath);
|
|
499
507
|
res.send({ message: 'Access denied', code: 403 });
|
|
500
508
|
return;
|
|
501
509
|
}
|
|
@@ -517,6 +525,17 @@ class DLServer {
|
|
|
517
525
|
utils_js_1.logger.error('Localplay file not found:', filepath);
|
|
518
526
|
res.status(404).send({ message: 'Not Found', code: 404 });
|
|
519
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
|
+
});
|
|
520
539
|
}
|
|
521
540
|
}
|
|
522
541
|
exports.DLServer = DLServer;
|
package/client/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
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
|
-
<title>M3U8
|
|
10
|
-
<link rel="icon" type="image/
|
|
9
|
+
<title>M3U8 下载器</title>
|
|
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"
|
|
@@ -406,6 +406,14 @@
|
|
|
406
406
|
class="text-blue-500 hover:text-blue-600">
|
|
407
407
|
{{serverInfo.version}}</a>
|
|
408
408
|
</p>
|
|
409
|
+
<p class="text-gray-600"><strong>检测版本:</strong>
|
|
410
|
+
<button @click="checkNewVersion" v-if="!serverInfo.appUpdateMessage"
|
|
411
|
+
class="px-2 py-1 text-sm bg-green-600 hover:bg-green-700 text-white rounded">
|
|
412
|
+
<i class="fas fa-check mr-1"></i>检测新版本
|
|
413
|
+
</button>
|
|
414
|
+
<span v-if="serverInfo.newVersion" class="text-blue-600">发现新版本![{{serverInfo.newVersion}}]</span>
|
|
415
|
+
<span v-if="serverInfo.appUpdateMessage" class="text-green-600">{{serverInfo.appUpdateMessage}}</span>
|
|
416
|
+
</p>
|
|
409
417
|
</div>
|
|
410
418
|
</div>
|
|
411
419
|
|
|
@@ -513,7 +521,10 @@ services:
|
|
|
513
521
|
alert(msg, p) {
|
|
514
522
|
p = typeof msg === 'object' ? msg : Object.assign({ text: msg }, p);
|
|
515
523
|
if (!p.toast) p.allowOutsideClick = false;
|
|
516
|
-
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
|
+
},
|
|
526
|
+
confirm(msg, p) {
|
|
527
|
+
return this.alert(msg, { showConfirmButton: true, showCancelButton: true, showCloseButton: true, confirmButtonText: '确认', cancelButtonText: '取消' });
|
|
517
528
|
},
|
|
518
529
|
toast(msg, p) {
|
|
519
530
|
p = (typeof msg === 'object' ? msg : Object.assign({ text: msg }, p));
|
|
@@ -602,6 +613,8 @@ services:
|
|
|
602
613
|
serverInfo: {
|
|
603
614
|
version: '{{version}}',
|
|
604
615
|
ariang: false,
|
|
616
|
+
newVersion: '',
|
|
617
|
+
appUpdateMessage: '',
|
|
605
618
|
},
|
|
606
619
|
config: {
|
|
607
620
|
/** 并发下载线程数。取决于服务器限制,过多可能会容易下载失败。一般建议不超过 8 个。默认为 cpu数 * 2,但不超过 8 */
|
|
@@ -661,7 +674,7 @@ services:
|
|
|
661
674
|
|
|
662
675
|
// 排序:resume > pending > pause > error > done
|
|
663
676
|
const statusOrder = { resume: 0, pending: 1, pause: 2, error: 3, done: 4 };
|
|
664
|
-
tasks.sort((a, b) => (statusOrder[a.status] - statusOrder[b.status]) || a.status === 'done' ? (b.filename - a.filename) : (b.endTime - a.endTime));
|
|
677
|
+
tasks.sort((a, b) => (statusOrder[a.status] - statusOrder[b.status]) || (a.status === 'done' ? (b.filename - a.filename) : (b.endTime - a.endTime)));
|
|
665
678
|
|
|
666
679
|
// 更新 queueStatus
|
|
667
680
|
const queueStatus = {
|
|
@@ -682,6 +695,38 @@ services:
|
|
|
682
695
|
}
|
|
683
696
|
},
|
|
684
697
|
methods: {
|
|
698
|
+
initEventsForApp() {
|
|
699
|
+
if (!window.electron) return;
|
|
700
|
+
const ipc = window.electron.ipc;
|
|
701
|
+
ipc.on('message', (ev) => {
|
|
702
|
+
if (typeof ev.data === 'string') T.toast(ev.data, { icon: 'info' });
|
|
703
|
+
else console.log(ev.data);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
ipc.on('downloadProgress', (data) => {
|
|
707
|
+
console.log('downloadProgress', data);
|
|
708
|
+
this.serverInfo.appUpdateMessage = `下载中:${Number(data.percent).toFixed(2)}% [${T.formatSpeed(data.bytesPerSecond)}] [${T.formatSize(data.transferred)}/${T.formatSize(data.total)}]`;
|
|
709
|
+
});
|
|
710
|
+
},
|
|
711
|
+
async checkNewVersion() {
|
|
712
|
+
try {
|
|
713
|
+
const r = await fetch(`https://registry.npmmirror.com/@lzwme/m3u8-dl/latest`).then(r => r.json());
|
|
714
|
+
if (r.version) {
|
|
715
|
+
if (r.version === this.serverInfo.version) T.toast(`已是最新版本,无需更新[${r.version}]`);
|
|
716
|
+
else {
|
|
717
|
+
this.serverInfo.newVersion = r.version;
|
|
718
|
+
if (window.electron) {
|
|
719
|
+
window.electron.ipc.send('checkForUpdate');
|
|
720
|
+
} else {
|
|
721
|
+
T.alert(`发现新版本[${r.version}],请前往 https://github.com/lzwme/m3u8-dl/releases 下载最新版本`, { icon: 'success' });
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
} catch (error) {
|
|
726
|
+
console.error('检查新版本失败:', error);
|
|
727
|
+
T.alert(`版本检查失败:${error.message}`, { icon: 'error' });
|
|
728
|
+
}
|
|
729
|
+
},
|
|
685
730
|
forceUpdate: function () {
|
|
686
731
|
const now = Date.now();
|
|
687
732
|
if (now - this.forceUpdateTime > 500) {
|
|
@@ -705,7 +750,7 @@ services:
|
|
|
705
750
|
|
|
706
751
|
switch (type) {
|
|
707
752
|
case 'serverInfo':
|
|
708
|
-
this.serverInfo
|
|
753
|
+
Object.assign(this.serverInfo, data);
|
|
709
754
|
break;
|
|
710
755
|
case 'tasks':
|
|
711
756
|
this.tasks = data;
|
|
@@ -745,7 +790,7 @@ services:
|
|
|
745
790
|
},
|
|
746
791
|
/** 获取配置 */
|
|
747
792
|
fetchConfig: async function () {
|
|
748
|
-
const config = await T.get('/config');
|
|
793
|
+
const config = await T.get('/api/config');
|
|
749
794
|
if (config.code) {
|
|
750
795
|
console.error('获取配置失败:', config);
|
|
751
796
|
T.alert('获取配置失败: ' + config.message, { icon: 'error' });
|
|
@@ -758,7 +803,7 @@ services:
|
|
|
758
803
|
},
|
|
759
804
|
/** 更新配置 */
|
|
760
805
|
updateConfig: async function () {
|
|
761
|
-
const result = await T.post('/config', this.config);
|
|
806
|
+
const result = await T.post('/api/config', this.config);
|
|
762
807
|
T.toast(result.message || '配置已更新', { icon: result.code ? 'error' : 'success' });
|
|
763
808
|
},
|
|
764
809
|
updateLocalConfig: async function () {
|
|
@@ -785,18 +830,31 @@ services:
|
|
|
785
830
|
width: '800px',
|
|
786
831
|
html: `
|
|
787
832
|
<div class="text-left">
|
|
788
|
-
<
|
|
789
|
-
|
|
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>
|
|
790
841
|
|
|
791
842
|
<div class="mt-4">
|
|
792
|
-
<label class="block text-sm font-bold text-gray-700 mb-1"
|
|
793
|
-
<
|
|
794
|
-
<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>
|
|
795
845
|
</div>
|
|
796
846
|
|
|
797
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">
|
|
798
856
|
<label class="block text-sm font-bold text-gray-700 mb-1">保存位置</label>
|
|
799
|
-
<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 || ''}">
|
|
800
858
|
</div>
|
|
801
859
|
|
|
802
860
|
<div class="mt-4">
|
|
@@ -806,7 +864,7 @@ services:
|
|
|
806
864
|
|
|
807
865
|
<div class="mt-4">
|
|
808
866
|
<label class="block text-sm font-bold text-gray-700 mb-1">自定义请求头</label>
|
|
809
|
-
<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>
|
|
810
868
|
</div>
|
|
811
869
|
</div>
|
|
812
870
|
`,
|
|
@@ -852,6 +910,31 @@ services:
|
|
|
852
910
|
}).then((result) => {
|
|
853
911
|
if (result.isConfirmed) this.startBatchDownload(result.value);
|
|
854
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);
|
|
855
938
|
},
|
|
856
939
|
/** 批量下载 */
|
|
857
940
|
startBatchDownload: async function (list) {
|
|
@@ -860,7 +943,7 @@ services:
|
|
|
860
943
|
Object.entries(item).forEach(([key, value]) => !value && delete item[key]);
|
|
861
944
|
this.tasks[item.url] = { status: 'resume', progress: 0, speed: 0, remainingTime: 0, size: 0 };
|
|
862
945
|
});
|
|
863
|
-
const r = await T.post('/download', { list });
|
|
946
|
+
const r = await T.post('/api/download', { list });
|
|
864
947
|
if (!r.code) T.toast(r.message || '批量下载已开始');
|
|
865
948
|
this.forceUpdate();
|
|
866
949
|
} catch (error) {
|
|
@@ -872,7 +955,7 @@ services:
|
|
|
872
955
|
pauseDownload: async function (urls) {
|
|
873
956
|
if (!urls) urls = this.selectedTasks;
|
|
874
957
|
if (typeof urls === 'string') urls = [urls];
|
|
875
|
-
const r = await T.post(
|
|
958
|
+
const r = await T.post('/api/pause', { urls, all: urls[0] === 'all' });
|
|
876
959
|
if (!r.code) T.toast(r.message || '已暂停下载');
|
|
877
960
|
if (urls === this.selectedTasks) this.selectedTasks = [];
|
|
878
961
|
},
|
|
@@ -880,7 +963,7 @@ services:
|
|
|
880
963
|
resumeDownload: async function (urls) {
|
|
881
964
|
if (!urls) urls = this.selectedTasks;
|
|
882
965
|
if (typeof urls === 'string') urls = [urls];
|
|
883
|
-
const r = await T.post(
|
|
966
|
+
const r = await T.post('/api/resume', { urls, all: urls[0] === 'all' });
|
|
884
967
|
if (!r.code) T.toast(r.message || '已恢复下载');
|
|
885
968
|
if (urls === this.selectedTasks) this.selectedTasks = [];
|
|
886
969
|
},
|
|
@@ -920,7 +1003,7 @@ services:
|
|
|
920
1003
|
});
|
|
921
1004
|
|
|
922
1005
|
if (result.isConfirmed) {
|
|
923
|
-
const r = await T.post(
|
|
1006
|
+
const r = await T.post('/api/delete', {
|
|
924
1007
|
urls,
|
|
925
1008
|
deleteCache: result.value.deleteCache,
|
|
926
1009
|
deleteVideo: result.value.deleteVideo
|
|
@@ -939,7 +1022,7 @@ services:
|
|
|
939
1022
|
}
|
|
940
1023
|
},
|
|
941
1024
|
getTasks: async function () {
|
|
942
|
-
this.tasks = await T.get('/tasks');
|
|
1025
|
+
this.tasks = await T.get('/api/tasks');
|
|
943
1026
|
},
|
|
944
1027
|
showTaskDetail(task) {
|
|
945
1028
|
console.log(task);
|
|
@@ -958,9 +1041,10 @@ services:
|
|
|
958
1041
|
预估还需: !task.endTime && task.remainingTime && T.formatTimeCost(task.remainingTime),
|
|
959
1042
|
相关信息: task.errmsg && `<span class=text-red-600>${task.errmsg}</span>`
|
|
960
1043
|
};
|
|
961
|
-
|
|
1044
|
+
T.alert({
|
|
962
1045
|
title: '任务详情',
|
|
963
1046
|
width: 1000,
|
|
1047
|
+
icon: '',
|
|
964
1048
|
html: [
|
|
965
1049
|
'<div class="flex-col full-width text-left">',
|
|
966
1050
|
Object.entries(taskInfo).filter(d => d[1]).map(
|
|
@@ -1009,12 +1093,12 @@ services:
|
|
|
1009
1093
|
},
|
|
1010
1094
|
// 更新任务优先级
|
|
1011
1095
|
updatePriority: async function (url, priority) {
|
|
1012
|
-
const r = await T.post('/priority', { url, priority: parseInt(priority) });
|
|
1096
|
+
const r = await T.post('/api/priority', { url, priority: parseInt(priority) });
|
|
1013
1097
|
T.toast(r.message || '已更新优先级');
|
|
1014
1098
|
},
|
|
1015
1099
|
// 获取队列状态
|
|
1016
1100
|
getQueueStatus: async function () {
|
|
1017
|
-
const status = await T.get('/queue/status');
|
|
1101
|
+
const status = await T.get('/api/queue/status');
|
|
1018
1102
|
if (status?.maxConcurrent) this.queueStatus = status;
|
|
1019
1103
|
},
|
|
1020
1104
|
// 清空下载队列
|
|
@@ -1030,7 +1114,7 @@ services:
|
|
|
1030
1114
|
});
|
|
1031
1115
|
|
|
1032
1116
|
if (result.isConfirmed) {
|
|
1033
|
-
const r = await T.post('/queue/clear');
|
|
1117
|
+
const r = await T.post('/api/queue/clear');
|
|
1034
1118
|
T.toast(r.message || '已清空下载队列');
|
|
1035
1119
|
this.getQueueStatus();
|
|
1036
1120
|
}
|
|
@@ -1058,6 +1142,7 @@ services:
|
|
|
1058
1142
|
T.reqHeaders.authorization = this.token ? `${this.token}` : '';
|
|
1059
1143
|
this.fetchConfig().then(d => d && this.wsConnect());
|
|
1060
1144
|
window.addEventListener('resize', this.handleResize);
|
|
1145
|
+
this.initEventsForApp();
|
|
1061
1146
|
},
|
|
1062
1147
|
beforeDestroy() {
|
|
1063
1148
|
window.removeEventListener('resize', this.handleResize);
|
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": "^
|
|
48
|
-
"@eslint/js": "^9.
|
|
47
|
+
"@biomejs/biome": "^2.0.6",
|
|
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.
|
|
56
|
-
"eslint": "^9.
|
|
54
|
+
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
|
55
|
+
"@typescript-eslint/parser": "^8.35.0",
|
|
56
|
+
"eslint": "^9.29.0",
|
|
57
57
|
"eslint-config-prettier": "^10.1.5",
|
|
58
|
-
"eslint-plugin-prettier": "^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>
|