@lzwme/m3u8-dl 1.2.2 → 1.3.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 +1 -0
- package/cjs/lib/m3u8-download.js +28 -1
- package/cjs/lib/parseM3u8.js +1 -1
- package/cjs/server/download-server.js +34 -22
- package/cjs/types/m3u8.d.ts +4 -0
- package/client/index.html +30 -152
- package/client/play.html +34 -33
- package/client/style.css +129 -0
- package/package.json +5 -3
package/cjs/cli.js
CHANGED
|
@@ -67,6 +67,7 @@ commander_1.program
|
|
|
67
67
|
.option('--no-convert', '下载成功后,是否不合并转换为 mp4 文件。默认为 true。')
|
|
68
68
|
.option('-H, --headers <headers>', '自定义请求头。格式为 key1=value1\nkey2=value2')
|
|
69
69
|
.option('-T, --type <type>', '指定下载类型。默认根据URL自动识别,如果是批量下载多个不同 URL 类型,请不要设置。可选值:m3u8, file, parser')
|
|
70
|
+
.option('-I, --ignore-segments <time-segments>', '忽略的视频片段,用-分割起始时间点,多个用逗号分隔。如:0-10,20-30')
|
|
70
71
|
.action(async (urls) => {
|
|
71
72
|
const options = getOptions();
|
|
72
73
|
utils_js_1.logger.debug(urls, options);
|
package/cjs/lib/m3u8-download.js
CHANGED
|
@@ -105,8 +105,35 @@ async function m3u8InfoParse(url, options = {}) {
|
|
|
105
105
|
utils_js_1.logger.error('[parseM3U8][failed]', e.message);
|
|
106
106
|
console.log(e);
|
|
107
107
|
});
|
|
108
|
-
if (m3u8Info && m3u8Info?.tsCount > 0)
|
|
108
|
+
if (m3u8Info && m3u8Info?.tsCount > 0) {
|
|
109
109
|
result.m3u8Info = m3u8Info;
|
|
110
|
+
if (options.ignoreSegments) {
|
|
111
|
+
const timeSegments = options.ignoreSegments
|
|
112
|
+
.split(',')
|
|
113
|
+
.map(d => d.split(/[- ]+/).map(d => +d.trim()))
|
|
114
|
+
.filter(d => d[0] && d[1] && d[0] !== d[1]);
|
|
115
|
+
if (timeSegments.length) {
|
|
116
|
+
const total = m3u8Info.data.length;
|
|
117
|
+
m3u8Info.data = m3u8Info.data.filter(item => {
|
|
118
|
+
for (let [start, end] of timeSegments) {
|
|
119
|
+
if (start > end)
|
|
120
|
+
[start, end] = [end, start];
|
|
121
|
+
if (item.timeline + item.duration / 2 >= start && item.timeline + item.duration / 2 <= end) {
|
|
122
|
+
m3u8Info.duration -= item.duration;
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
});
|
|
128
|
+
const ignoredCount = total - m3u8Info.data.length;
|
|
129
|
+
if (ignoredCount) {
|
|
130
|
+
m3u8Info.tsCount = m3u8Info.data.length;
|
|
131
|
+
utils_js_1.logger.info(`[parseM3U8][ignoreSegments] ignored ${(0, console_log_colors_1.cyanBright)(ignoredCount)} segments`);
|
|
132
|
+
m3u8Info.duration = +Number(m3u8Info.duration).toFixed(2);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
110
137
|
return result;
|
|
111
138
|
}
|
|
112
139
|
/**
|
package/cjs/lib/parseM3u8.js
CHANGED
|
@@ -83,7 +83,7 @@ async function parseM3U8(content, cacheDir = './cache', headers) {
|
|
|
83
83
|
result.data.push({
|
|
84
84
|
index: i,
|
|
85
85
|
duration: item.duration,
|
|
86
|
-
timeline: item.timeline,
|
|
86
|
+
timeline: item.timeline || result.duration,
|
|
87
87
|
uri: item.uri,
|
|
88
88
|
tsOut: (0, node_path_1.resolve)(cacheDir, `${(0, fe_utils_1.md5)(item.uri)}.ts`),
|
|
89
89
|
keyUri: item.key?.uri || '',
|
|
@@ -43,6 +43,7 @@ const format_options_js_1 = require("../lib/format-options.js");
|
|
|
43
43
|
const m3u8_download_js_1 = require("../lib/m3u8-download.js");
|
|
44
44
|
const utils_js_1 = require("../lib/utils.js");
|
|
45
45
|
const index_js_1 = require("../video-parser/index.js");
|
|
46
|
+
const rootDir = (0, node_path_1.resolve)(__dirname, '../..');
|
|
46
47
|
class DLServer {
|
|
47
48
|
app = null;
|
|
48
49
|
wss = null;
|
|
@@ -55,7 +56,7 @@ class DLServer {
|
|
|
55
56
|
};
|
|
56
57
|
serverInfo = {
|
|
57
58
|
version: '',
|
|
58
|
-
ariang: (0, node_fs_1.existsSync)((0, node_path_1.resolve)(
|
|
59
|
+
ariang: (0, node_fs_1.existsSync)((0, node_path_1.resolve)(rootDir, 'client/ariang/index.html')),
|
|
59
60
|
};
|
|
60
61
|
cfg = {
|
|
61
62
|
/** 支持 web 设置修改的参数 */
|
|
@@ -85,7 +86,7 @@ class DLServer {
|
|
|
85
86
|
opts.cacheDir = (0, node_path_1.resolve)(opts.cacheDir);
|
|
86
87
|
if (!opts.configPath)
|
|
87
88
|
opts.configPath = (0, node_path_1.resolve)(opts.cacheDir, 'config.json');
|
|
88
|
-
const pkgFile = (0, node_path_1.resolve)(
|
|
89
|
+
const pkgFile = (0, node_path_1.resolve)(rootDir, 'package.json');
|
|
89
90
|
if ((0, node_fs_1.existsSync)(pkgFile)) {
|
|
90
91
|
const pkg = JSON.parse((0, node_fs_1.readFileSync)(pkgFile, 'utf8'));
|
|
91
92
|
this.serverInfo.version = pkg.version;
|
|
@@ -187,8 +188,23 @@ class DLServer {
|
|
|
187
188
|
const wss = new WebSocketServer({ server });
|
|
188
189
|
this.app = app;
|
|
189
190
|
this.wss = wss;
|
|
191
|
+
app.use((req, res, next) => {
|
|
192
|
+
if (['/', '/index.html'].includes(req.path)) {
|
|
193
|
+
const version = this.serverInfo.version;
|
|
194
|
+
let indexHtml = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(rootDir, 'client/index.html'), 'utf-8').replaceAll('{{version}}', version);
|
|
195
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(rootDir, 'client/local/cdn'))) {
|
|
196
|
+
indexHtml = indexHtml
|
|
197
|
+
.replaceAll('https://s4.zstatic.net/ajax/libs', 'local/cdn')
|
|
198
|
+
.replace('https://cdn.tailwindcss.com/3.4.16', 'local/cdn/tailwindcss/3.4.16/tailwindcss.min.js');
|
|
199
|
+
}
|
|
200
|
+
res.setHeader('content-type', 'text/html').send(indexHtml);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
next();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
190
206
|
app.use(express.json());
|
|
191
|
-
app.use(express.static((0, node_path_1.resolve)(
|
|
207
|
+
app.use(express.static((0, node_path_1.resolve)(rootDir, 'client')));
|
|
192
208
|
app.use((req, res, next) => {
|
|
193
209
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
194
210
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
|
|
@@ -230,20 +246,19 @@ class DLServer {
|
|
|
230
246
|
return { app, wss };
|
|
231
247
|
}
|
|
232
248
|
startDownload(url, options) {
|
|
233
|
-
const cacheItem = this.dlCache.get(url);
|
|
234
249
|
const dlOptions = (0, format_options_js_1.formatOptions)(url, { ...this.cfg.dlOptions, ...options, cacheDir: this.options.cacheDir })[1];
|
|
250
|
+
const cacheItem = this.dlCache.get(url) || { options, dlOptions, status: 'pending', url };
|
|
235
251
|
utils_js_1.logger.debug('startDownload', url, dlOptions, cacheItem?.status);
|
|
236
|
-
if (cacheItem
|
|
252
|
+
if (cacheItem.status === 'resume')
|
|
237
253
|
return cacheItem.options;
|
|
238
254
|
if (this.downloading >= this.cfg.webOptions.maxDownloads) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
this.dlCache.set(url, { options, dlOptions, status: 'pending', url });
|
|
243
|
-
return cacheItem?.options || dlOptions;
|
|
255
|
+
cacheItem.status = 'pending';
|
|
256
|
+
this.dlCache.set(url, cacheItem);
|
|
257
|
+
return cacheItem.options;
|
|
244
258
|
}
|
|
245
|
-
|
|
246
|
-
|
|
259
|
+
cacheItem.status = 'resume';
|
|
260
|
+
this.dlCache.set(url, cacheItem);
|
|
261
|
+
let workPoll = cacheItem.workPoll;
|
|
247
262
|
const opts = {
|
|
248
263
|
...dlOptions,
|
|
249
264
|
showProgress: dlOptions.debug || this.options.debug,
|
|
@@ -251,8 +266,8 @@ class DLServer {
|
|
|
251
266
|
workPoll = wp;
|
|
252
267
|
},
|
|
253
268
|
onProgress: (_finished, _total, current, stats) => {
|
|
254
|
-
const item = this.dlCache.get(url) ||
|
|
255
|
-
const status = item
|
|
269
|
+
const item = this.dlCache.get(url) || cacheItem;
|
|
270
|
+
const status = item.status || 'resume';
|
|
256
271
|
Object.assign(item, { ...stats, current, options: dlOptions, status, workPoll, url });
|
|
257
272
|
this.dlCache.set(url, item);
|
|
258
273
|
this.saveCache();
|
|
@@ -261,7 +276,7 @@ class DLServer {
|
|
|
261
276
|
},
|
|
262
277
|
};
|
|
263
278
|
const afterDownload = (r, url) => {
|
|
264
|
-
const item = this.dlCache.get(url) ||
|
|
279
|
+
const item = this.dlCache.get(url) || cacheItem;
|
|
265
280
|
if (r.filepath && (0, node_fs_1.existsSync)(r.filepath)) {
|
|
266
281
|
item.localVideo = r.filepath;
|
|
267
282
|
item.downloadedSize = (0, node_fs_1.statSync)(r.filepath).size;
|
|
@@ -284,10 +299,6 @@ class DLServer {
|
|
|
284
299
|
}
|
|
285
300
|
}
|
|
286
301
|
};
|
|
287
|
-
if (cacheItem)
|
|
288
|
-
cacheItem.status = 'resume';
|
|
289
|
-
else
|
|
290
|
-
this.dlCache.set(url, defaultItem);
|
|
291
302
|
try {
|
|
292
303
|
if (dlOptions.type === 'parser') {
|
|
293
304
|
const vp = new index_js_1.VideoParser();
|
|
@@ -404,11 +415,12 @@ class DLServer {
|
|
|
404
415
|
const urlsToPause = all ? [...this.dlCache.keys()] : urls;
|
|
405
416
|
const list = [];
|
|
406
417
|
for (const url of urlsToPause) {
|
|
407
|
-
const
|
|
418
|
+
const item = this.dlCache.get(url);
|
|
408
419
|
if (['resume', 'pending'].includes(item?.status)) {
|
|
409
|
-
(0, m3u8_download_js_1.m3u8DLStop)(url, workPoll);
|
|
420
|
+
(0, m3u8_download_js_1.m3u8DLStop)(url, item.workPoll);
|
|
410
421
|
item.status = item.tsSuccess === item.tsCount ? 'done' : 'pause';
|
|
411
|
-
|
|
422
|
+
const { workPoll, ...tItem } = item;
|
|
423
|
+
list.push(tItem);
|
|
412
424
|
}
|
|
413
425
|
}
|
|
414
426
|
if (list.length)
|
package/cjs/types/m3u8.d.ts
CHANGED
|
@@ -6,7 +6,9 @@ export interface TsItemInfo {
|
|
|
6
6
|
m3u8: string;
|
|
7
7
|
/** ts 文件次序 */
|
|
8
8
|
index: number;
|
|
9
|
+
/** 视频片段时长 */
|
|
9
10
|
duration: number;
|
|
11
|
+
/** 时间线(起点) */
|
|
10
12
|
timeline: number;
|
|
11
13
|
/** ts 文件下载 url 地址 */
|
|
12
14
|
uri: string;
|
|
@@ -119,6 +121,8 @@ export interface M3u8DLOptions {
|
|
|
119
121
|
saveDir?: string;
|
|
120
122
|
/** 临时文件保存目录。默认为 cache/<md5(url)> */
|
|
121
123
|
cacheDir?: string;
|
|
124
|
+
/** 忽略的时间片段,单位为秒,多段以逗号分割。示例: 0-10,100-110 */
|
|
125
|
+
ignoreSegments?: string;
|
|
122
126
|
/** 下载成功后是否删除临时文件。默认为 true。保存临时文件可以在重复下载时识别缓存 */
|
|
123
127
|
delCache?: boolean;
|
|
124
128
|
/** 文件已存在时是否仍强制下载和生成。默认为 false,文件已存在则跳过 */
|
package/client/index.html
CHANGED
|
@@ -8,154 +8,24 @@
|
|
|
8
8
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
9
9
|
<title>M3U8 下载管理</title>
|
|
10
10
|
<link rel="icon" type="image/svg+xml" href="logo.svg">
|
|
11
|
-
<
|
|
11
|
+
<link rel="stylesheet" href="https://s4.zstatic.net/ajax/libs/font-awesome/6.7.2/css/all.min.css"
|
|
12
|
+
integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg=="
|
|
13
|
+
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
|
14
|
+
<link rel="stylesheet" href="https://s4.zstatic.net/ajax/libs/sweetalert2/11.16.1/sweetalert2.min.css"
|
|
15
|
+
integrity="sha512-WnmDqbbAeHb7Put2nIAp7KNlnMup0FXVviOctducz1omuXB/hHK3s2vd3QLffK/CvvFUKrpioxdo+/Jo3k/xIw=="
|
|
16
|
+
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
|
17
|
+
<link rel="stylesheet" href="style.css?v={{version}}">
|
|
18
|
+
|
|
19
|
+
<script src="https://cdn.tailwindcss.com/3.4.16"></script>
|
|
12
20
|
<script src="https://s4.zstatic.net/ajax/libs/vue/2.7.16/vue.min.js"
|
|
13
21
|
integrity="sha512-Wx8niGbPNCD87mSuF0sBRytwW2+2ZFr7HwVDF8krCb3egstCc4oQfig+/cfg2OHd82KcUlOYxlSDAqdHqK5TCw=="
|
|
14
22
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
15
23
|
<script src="https://s4.zstatic.net/ajax/libs/sweetalert2/11.16.1/sweetalert2.min.js"
|
|
16
24
|
integrity="sha512-LGHBR+kJ5jZSIzhhdfytPoEHzgaYuTRifq9g5l6ja6/k9NAOsAi5dQh4zQF6JIRB8cAYxTRedERUF+97/KuivQ=="
|
|
17
25
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
18
|
-
<script src="https://s4.zstatic.net/ajax/libs/blueimp-md5/2.19.0/js/md5.min.js"
|
|
19
|
-
|
|
20
|
-
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
21
|
-
<link rel="stylesheet" href="https://s4.zstatic.net/ajax/libs/sweetalert2/11.16.1/sweetalert2.min.css"
|
|
22
|
-
integrity="sha512-WnmDqbbAeHb7Put2nIAp7KNlnMup0FXVviOctducz1omuXB/hHK3s2vd3QLffK/CvvFUKrpioxdo+/Jo3k/xIw=="
|
|
23
|
-
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
|
24
|
-
<link rel="stylesheet" href="https://s4.zstatic.net/ajax/libs/font-awesome/6.7.2/css/all.min.css"
|
|
25
|
-
integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg=="
|
|
26
|
-
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
|
27
|
-
|
|
28
|
-
<style>
|
|
29
|
-
#app {
|
|
30
|
-
max-width: 1400px;
|
|
31
|
-
margin: auto;
|
|
32
|
-
min-height: 100vh;
|
|
33
|
-
background-color: #f5f5f5;
|
|
34
|
-
position: relative;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.sidebar {
|
|
38
|
-
background-color: #fff;
|
|
39
|
-
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
|
40
|
-
transition: all 0.3s ease;
|
|
41
|
-
position: absolute;
|
|
42
|
-
left: 0;
|
|
43
|
-
top: 0;
|
|
44
|
-
bottom: 0;
|
|
45
|
-
width: 16rem;
|
|
46
|
-
z-index: 1;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.download-item {
|
|
50
|
-
transition: all 0.3s ease;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.download-item:hover {
|
|
54
|
-
background-color: #f8f9fa;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.progress-bar {
|
|
58
|
-
height: 4px;
|
|
59
|
-
transition: width 0.3s ease;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.nav-item {
|
|
63
|
-
transition: all 0.3s ease;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.nav-item:hover {
|
|
67
|
-
background-color: #f0f0f0;
|
|
68
|
-
transform: translateX(4px);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.nav-item.active {
|
|
72
|
-
background-color: #e6f3ff;
|
|
73
|
-
color: #1890ff;
|
|
74
|
-
}
|
|
26
|
+
<script src="https://s4.zstatic.net/ajax/libs/blueimp-md5/2.19.0/js/md5.min.js" crossorigin="anonymous"
|
|
27
|
+
referrerpolicy="no-referrer"></script>
|
|
75
28
|
|
|
76
|
-
/* 移动端适配 */
|
|
77
|
-
@media (max-width: 768px) {
|
|
78
|
-
.sidebar {
|
|
79
|
-
transform: translateX(-100%);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.sidebar.show {
|
|
83
|
-
transform: translateX(0);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.menu-toggle {
|
|
87
|
-
display: block !important;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
.main-content {
|
|
91
|
-
margin-left: 0 !important;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
.menu-toggle {
|
|
96
|
-
display: none;
|
|
97
|
-
position: fixed;
|
|
98
|
-
top: 1rem;
|
|
99
|
-
left: 1rem;
|
|
100
|
-
z-index: 51;
|
|
101
|
-
padding: 0.5rem;
|
|
102
|
-
background: white;
|
|
103
|
-
border-radius: 0.5rem;
|
|
104
|
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
105
|
-
box-shadow: 0 2px 4px #9bdff5;
|
|
106
|
-
width: 42px;
|
|
107
|
-
height: 42px;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.main-content {
|
|
111
|
-
transition: margin-left 0.3s ease;
|
|
112
|
-
margin-left: 16rem;
|
|
113
|
-
width: calc(100% - 16rem);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/* 自定义toast样式 */
|
|
117
|
-
.custom-toast {
|
|
118
|
-
position: fixed;
|
|
119
|
-
top: 20px;
|
|
120
|
-
right: 20px;
|
|
121
|
-
min-width: 250px;
|
|
122
|
-
max-width: 400px;
|
|
123
|
-
padding: 15px;
|
|
124
|
-
border-radius: 4px;
|
|
125
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
126
|
-
background: #fff;
|
|
127
|
-
color: #333;
|
|
128
|
-
z-index: 99999;
|
|
129
|
-
display: flex;
|
|
130
|
-
align-items: center;
|
|
131
|
-
transform: translateX(150%);
|
|
132
|
-
transition: transform 0.3s ease;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
.custom-toast.show {
|
|
136
|
-
transform: translateX(0);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
.custom-toast.hide {
|
|
140
|
-
transform: translateX(150%);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
.custom-toast-success {
|
|
144
|
-
border-left: 4px solid #28a745;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
.custom-toast-error {
|
|
148
|
-
border-left: 4px solid #dc3545;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.custom-toast-warning {
|
|
152
|
-
border-left: 4px solid #ffc107;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
.custom-toast-info {
|
|
156
|
-
border-left: 4px solid #17a2b8;
|
|
157
|
-
}
|
|
158
|
-
</style>
|
|
159
29
|
</head>
|
|
160
30
|
|
|
161
31
|
<body>
|
|
@@ -343,16 +213,6 @@
|
|
|
343
213
|
<i class="fas fa-hourglass-half mr-1"></i>
|
|
344
214
|
<span>剩余: {{ T.formatTimeCost(task.remainingTime || 0) }}</span>
|
|
345
215
|
</span>
|
|
346
|
-
<!-- <span class="flex items-center">
|
|
347
|
-
<i class="fas fa-sort-amount-up mr-1"></i>
|
|
348
|
-
<select v-model="task.priority" @change="updatePriority(url, task.priority)"
|
|
349
|
-
class="text-sm border rounded px-1 py-0.5 focus:ring-blue-500 focus:border-blue-500"
|
|
350
|
-
aria-label="设置下载优先级">
|
|
351
|
-
<option value="0">普通优先级</option>
|
|
352
|
-
<option value="1">高优先级</option>
|
|
353
|
-
<option value="2">最高优先级</option>
|
|
354
|
-
</select>
|
|
355
|
-
</span> -->
|
|
356
216
|
</div>
|
|
357
217
|
</div>
|
|
358
218
|
<div class="flex space-x-2">
|
|
@@ -622,6 +482,13 @@ services:
|
|
|
622
482
|
'content-type': 'application/json',
|
|
623
483
|
authorization: localStorage.getItem('token') || '',
|
|
624
484
|
},
|
|
485
|
+
initTJ() {
|
|
486
|
+
if (!window._hmt) window._hmt = [];
|
|
487
|
+
const hm = document.createElement("script");
|
|
488
|
+
hm.src = "https://hm.baidu.com/hm.js?0b21eda331ac9677a4c546dea88616d0";
|
|
489
|
+
const s = document.getElementsByTagName("script")[0];
|
|
490
|
+
s.parentNode.insertBefore(hm, s);
|
|
491
|
+
},
|
|
625
492
|
request(method, url, data, headers = {}) {
|
|
626
493
|
return fetch(url, {
|
|
627
494
|
method,
|
|
@@ -727,12 +594,13 @@ services:
|
|
|
727
594
|
};
|
|
728
595
|
|
|
729
596
|
Vue.prototype.T = T;
|
|
597
|
+
T.initTJ();
|
|
730
598
|
window.APP = new Vue({
|
|
731
599
|
el: '#app',
|
|
732
600
|
data: {
|
|
733
601
|
ws: null,
|
|
734
602
|
serverInfo: {
|
|
735
|
-
version: '',
|
|
603
|
+
version: '{{version}}',
|
|
736
604
|
ariang: false,
|
|
737
605
|
},
|
|
738
606
|
config: {
|
|
@@ -818,6 +686,7 @@ services:
|
|
|
818
686
|
const now = Date.now();
|
|
819
687
|
if (now - this.forceUpdateTime > 500) {
|
|
820
688
|
this.forceUpdateTime = now;
|
|
689
|
+
this.tasks = { ...this.tasks };
|
|
821
690
|
this.$forceUpdate();
|
|
822
691
|
} else {
|
|
823
692
|
if (this.forceUpdateTimeout) clearTimeout(this.forceUpdateTimeout);
|
|
@@ -930,6 +799,11 @@ services:
|
|
|
930
799
|
<input id="saveDir" class="w-full p-2 border rounded-lg focus:ring-blue-500" placeholder="请输入保存路径" value="${this.config.saveDir || ''}">
|
|
931
800
|
</div>
|
|
932
801
|
|
|
802
|
+
<div class="mt-4">
|
|
803
|
+
<label class="block text-sm font-bold text-gray-700 mb-1">删除时间片段(适用于移除广告片段的情况)</label>
|
|
804
|
+
<input id="ignoreSegments" class="w-full p-2 border rounded-lg focus:ring-blue-500" placeholder="以-分割起止时间,多个以逗号分隔。示例:0-10,20-100">
|
|
805
|
+
</div>
|
|
806
|
+
|
|
933
807
|
<div class="mt-4">
|
|
934
808
|
<label class="block text-sm font-bold text-gray-700 mb-1">自定义请求头</label>
|
|
935
809
|
<textarea id="headers" class="w-full p-2 border rounded-lg focus:ring-blue-500" rows="4" placeholder="每行一个请求头(微博视频必须设置 Cookie),格式:Key: Value 例如: Referer: https://example.com Cookie: token=123"></textarea>
|
|
@@ -947,6 +821,7 @@ services:
|
|
|
947
821
|
const filename = document.getElementById('filename').value.trim();
|
|
948
822
|
const saveDir = document.getElementById('saveDir').value.trim();
|
|
949
823
|
const headersText = document.getElementById('headers').value.trim();
|
|
824
|
+
const ignoreSegments = document.getElementById('ignoreSegments').value.trim();
|
|
950
825
|
|
|
951
826
|
if (!urlsText) {
|
|
952
827
|
Swal.showValidationMessage('请输入至少一个 M3U8 链接');
|
|
@@ -971,6 +846,7 @@ services:
|
|
|
971
846
|
filename: item.name || (filename ? `${filename}第${idx + 1}集` : ''),
|
|
972
847
|
saveDir,
|
|
973
848
|
headers,
|
|
849
|
+
ignoreSegments,
|
|
974
850
|
}));
|
|
975
851
|
}
|
|
976
852
|
}).then((result) => {
|
|
@@ -981,6 +857,7 @@ services:
|
|
|
981
857
|
startBatchDownload: async function (list) {
|
|
982
858
|
try {
|
|
983
859
|
list.forEach(async (item, idx) => {
|
|
860
|
+
Object.entries(item).forEach(([key, value]) => !value && delete item[key]);
|
|
984
861
|
this.tasks[item.url] = { status: 'resume', progress: 0, speed: 0, remainingTime: 0, size: 0 };
|
|
985
862
|
});
|
|
986
863
|
const r = await T.post('/download', { list });
|
|
@@ -1048,6 +925,7 @@ services:
|
|
|
1048
925
|
deleteCache: result.value.deleteCache,
|
|
1049
926
|
deleteVideo: result.value.deleteVideo
|
|
1050
927
|
});
|
|
928
|
+
|
|
1051
929
|
if (!r.code) {
|
|
1052
930
|
T.toast(r.message || '已删除选中的下载');
|
|
1053
931
|
urls.forEach(url => (delete this.tasks[url]));
|
package/client/play.html
CHANGED
|
@@ -2,43 +2,44 @@
|
|
|
2
2
|
<html lang="zh">
|
|
3
3
|
|
|
4
4
|
<head>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
<title>M3U8 在线播放</title>
|
|
6
|
+
<meta charset="utf-8">
|
|
7
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
8
|
+
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0,viewport-fit=cover"
|
|
9
|
+
name="viewport" />
|
|
10
|
+
<style>
|
|
11
|
+
body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
}
|
|
14
|
+
</style>
|
|
15
15
|
</head>
|
|
16
16
|
|
|
17
17
|
<body>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
<div id="dplayer"></div>
|
|
19
|
+
<script src="https://s4.zstatic.net/ajax/libs/hls.js/1.5.18/hls.min.js"
|
|
20
|
+
integrity="sha512-hARxLWym80kd0Bzl5/93OuW1ujaKfvmJ90yTKak/RB67JuNIjtErU2H7H3bteyfzMuqiSK0tXarT7eK6lEWBBA=="
|
|
21
|
+
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
22
|
+
<script src="https://s4.zstatic.net/ajax/libs/dplayer/1.26.0/DPlayer.min.js" crossorigin="anonymous"
|
|
23
|
+
referrerpolicy="no-referrer"></script>
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
25
|
+
<script>
|
|
26
|
+
const url = location.href.split('url=')[1];
|
|
27
|
+
if (!url) {
|
|
28
|
+
document.getElementById('dplayer').innerText = '请传入播放地址参数 url=';
|
|
29
|
+
} else {
|
|
30
|
+
const dp = new DPlayer({
|
|
31
|
+
container: document.getElementById('dplayer'),
|
|
32
|
+
autoplay: true,
|
|
33
|
+
video: {
|
|
34
|
+
url: decodeURIComponent(url),
|
|
35
|
+
type: 'auto',
|
|
36
|
+
},
|
|
37
|
+
pluginOptions: {
|
|
38
|
+
hls: {},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
42
43
|
</body>
|
|
43
44
|
|
|
44
45
|
</html>
|
package/client/style.css
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#app {
|
|
2
|
+
max-width: 1400px;
|
|
3
|
+
margin: auto;
|
|
4
|
+
min-height: 100vh;
|
|
5
|
+
background-color: #f5f5f5;
|
|
6
|
+
position: relative;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.sidebar {
|
|
10
|
+
background-color: #fff;
|
|
11
|
+
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
|
12
|
+
transition: all 0.3s ease;
|
|
13
|
+
position: absolute;
|
|
14
|
+
left: 0;
|
|
15
|
+
top: 0;
|
|
16
|
+
bottom: 0;
|
|
17
|
+
width: 16rem;
|
|
18
|
+
z-index: 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.download-item {
|
|
22
|
+
transition: all 0.3s ease;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.download-item:hover {
|
|
26
|
+
background-color: #f8f9fa;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.progress-bar {
|
|
30
|
+
height: 4px;
|
|
31
|
+
transition: width 0.3s ease;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.nav-item {
|
|
35
|
+
transition: all 0.3s ease;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.nav-item:hover {
|
|
39
|
+
background-color: #f0f0f0;
|
|
40
|
+
transform: translateX(4px);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.nav-item.active {
|
|
44
|
+
background-color: #e6f3ff;
|
|
45
|
+
color: #1890ff;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* 移动端适配 */
|
|
49
|
+
@media (max-width: 768px) {
|
|
50
|
+
.sidebar {
|
|
51
|
+
transform: translateX(-100%);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.sidebar.show {
|
|
55
|
+
transform: translateX(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.menu-toggle {
|
|
59
|
+
display: block !important;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.main-content {
|
|
63
|
+
margin-left: 0 !important;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.menu-toggle {
|
|
68
|
+
display: none;
|
|
69
|
+
position: fixed;
|
|
70
|
+
top: 1rem;
|
|
71
|
+
left: 1rem;
|
|
72
|
+
z-index: 51;
|
|
73
|
+
padding: 0.5rem;
|
|
74
|
+
background: white;
|
|
75
|
+
border-radius: 0.5rem;
|
|
76
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
77
|
+
box-shadow: 0 2px 4px #9bdff5;
|
|
78
|
+
width: 42px;
|
|
79
|
+
height: 42px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.main-content {
|
|
83
|
+
transition: margin-left 0.3s ease;
|
|
84
|
+
margin-left: 16rem;
|
|
85
|
+
width: calc(100% - 16rem);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* 自定义toast样式 */
|
|
89
|
+
.custom-toast {
|
|
90
|
+
position: fixed;
|
|
91
|
+
top: 20px;
|
|
92
|
+
right: 20px;
|
|
93
|
+
min-width: 250px;
|
|
94
|
+
max-width: 400px;
|
|
95
|
+
padding: 15px 25px 15px 15px;
|
|
96
|
+
border-radius: 4px;
|
|
97
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
98
|
+
background: #fff;
|
|
99
|
+
color: #333;
|
|
100
|
+
z-index: 99999;
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
transform: translateX(150%);
|
|
104
|
+
transition: transform 0.3s ease;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.custom-toast.show {
|
|
108
|
+
transform: translateX(0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.custom-toast.hide {
|
|
112
|
+
transform: translateX(150%);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.custom-toast-success {
|
|
116
|
+
border-left: 4px solid #28a745;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.custom-toast-error {
|
|
120
|
+
border-left: 4px solid #dc3545;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.custom-toast-warning {
|
|
124
|
+
border-left: 4px solid #ffc107;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.custom-toast-info {
|
|
128
|
+
border-left: 4px solid #17a2b8;
|
|
129
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lzwme/m3u8-dl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|
|
@@ -49,7 +49,7 @@
|
|
|
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": "^
|
|
52
|
+
"@types/node": "^24.0.1",
|
|
53
53
|
"@types/ws": "^8.18.1",
|
|
54
54
|
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
|
55
55
|
"@typescript-eslint/parser": "^8.34.0",
|
|
@@ -73,7 +73,9 @@
|
|
|
73
73
|
},
|
|
74
74
|
"files": [
|
|
75
75
|
"cjs",
|
|
76
|
-
"client",
|
|
76
|
+
"client/",
|
|
77
|
+
"!client/ariang",
|
|
78
|
+
"!client/local",
|
|
77
79
|
"!cjs/type.js",
|
|
78
80
|
"!cjs/cli.d.ts",
|
|
79
81
|
"bin"
|