@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 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);
@@ -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
  /**
@@ -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)(__dirname, '../../client/ariang/index.html')),
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)(__dirname, '../../package.json');
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)(__dirname, '../../client')));
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?.status === 'resume')
252
+ if (cacheItem.status === 'resume')
237
253
  return cacheItem.options;
238
254
  if (this.downloading >= this.cfg.webOptions.maxDownloads) {
239
- if (cacheItem)
240
- cacheItem.status = 'pending';
241
- else
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
- let workPoll = cacheItem?.workPoll;
246
- const defaultItem = { options, dlOptions, status: 'resume', url };
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) || defaultItem;
255
- const status = item?.status || 'resume';
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) || defaultItem;
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 { workPoll, ...item } = this.dlCache.get(url);
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
- list.push(item);
422
+ const { workPoll, ...tItem } = item;
423
+ list.push(tItem);
412
424
  }
413
425
  }
414
426
  if (list.length)
@@ -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
- <script src="https://cdn.tailwindcss.com"></script>
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
- integrity="sha512-8pbzenDolL1l5OPSsoURCx9TEdMFTaeFipASVrMYKhuYtly+k3tcsQYliOEKTmuB1t7yuzAiVo+yd7SJz+ijFQ=="
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&#10;例如:&#10;Referer: https://example.com&#10;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
- <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" name="viewport" />
9
- <link crossorigin="anonymous" rel="stylesheet" href="https://lib.baomitu.com/dplayer/latest/DPlayer.min.css">
10
- <style>
11
- body {
12
- margin: 0;
13
- }
14
- </style>
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
- <div id="dplayer"></div>
19
- <script crossorigin="anonymous"
20
- integrity="sha512-yi//c0pOEPlBEqUMgK7Ia1VXQT9TwuMHJIRU+T2lyV7YxsMhbF35N/DGYkCFWfC9ebjdupP4xadFyFVTz/sgEg=="
21
- src="https://lib.baomitu.com/hls.js/1.3.0/hls.min.js"></script>
22
- <script crossorigin="anonymous" src="https://lib.baomitu.com/dplayer/latest/DPlayer.min.js"></script>
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
- <script>
25
- const url = location.href.split('url=')[1];
26
- if (!url) {
27
- document.getElementById('dplayer').innerText = '请传入播放地址参数 url=';
28
- } else {
29
- const dp = new DPlayer({
30
- container: document.getElementById('dplayer'),
31
- autoplay: true,
32
- video: {
33
- url: decodeURIComponent(url),
34
- type: 'auto',
35
- },
36
- pluginOptions: {
37
- hls: {},
38
- },
39
- });
40
- }
41
- </script>
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>
@@ -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.2.2",
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": "^22.15.30",
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"