@lzwme/m3u8-dl 1.1.1 → 1.1.2

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.
@@ -17,10 +17,10 @@ async function m3u8Convert(options, data) {
17
17
  (0, fe_utils_1.mkdirp)((0, node_path_1.dirname)(filepath));
18
18
  if (ffmpegSupport) {
19
19
  const ffconcatFile = (0, node_path_1.resolve)((0, node_path_1.dirname)(data[0].tsOut), 'ffconcat.txt');
20
- let filesAllArr = data.map(d => (0, node_path_1.resolve)(d.tsOut)).filter(d => (0, node_fs_1.existsSync)(d));
20
+ let filesAllArr = data.filter(d => (0, node_fs_1.existsSync)(d.tsOut)).map(d => `file '${d.tsOut}'\nduration ${d.duration}`);
21
21
  if (process.platform === 'win32')
22
22
  filesAllArr = filesAllArr.map(d => d.replaceAll('\\', '/'));
23
- (0, node_fs_1.writeFileSync)(ffconcatFile, 'ffconcat version 1.0\nfile ' + filesAllArr.join('\nfile '));
23
+ (0, node_fs_1.writeFileSync)(ffconcatFile, 'ffconcat version 1.0\n' + filesAllArr.join('\n'));
24
24
  let headersString = '';
25
25
  if (options.headers) {
26
26
  for (const [key, value] of Object.entries(options.headers)) {
@@ -28,13 +28,15 @@ async function m3u8Convert(options, data) {
28
28
  }
29
29
  }
30
30
  // ffmpeg -i nz.ts -c copy -map 0:v -map 0:a -bsf:a aac_adtstoasc nz.mp4
31
- const cmd = `ffmpeg -y -f concat -safe 0 -i "${ffconcatFile}" -acodec copy -vcodec copy -bsf:a aac_adtstoasc ${headersString} "${filepath}"`;
31
+ // const cmd = `ffmpeg -async 1 -y -f concat -safe 0 -i "${ffconcatFile}" -acodec copy -vcodec copy -bsf:a aac_adtstoasc ${headersString} "${filepath}"`;
32
+ const cmd = `ffmpeg -async 1 -y -f concat -safe 0 -i "${ffconcatFile}" -c:v copy -c:a copy -movflags +faststart -fflags +genpts -bsf:a aac_adtstoasc ${headersString} "${filepath}"`;
32
33
  utils_1.logger.debug('[convert to mp4]cmd:', (0, console_log_colors_1.cyan)(cmd));
33
34
  const r = (0, fe_utils_1.execSync)(cmd);
34
35
  ffmpegSupport = !r.error;
35
36
  if (r.error)
36
37
  utils_1.logger.error('Conversion to mp4 failed. Please confirm that `ffmpeg` is installed!', r.stderr);
37
- (0, node_fs_1.unlinkSync)(ffconcatFile);
38
+ else
39
+ (0, node_fs_1.unlinkSync)(ffconcatFile);
38
40
  }
39
41
  if (!ffmpegSupport) {
40
42
  filepath = filepath.replace(/\.mp4$/, '.ts');
@@ -185,6 +185,7 @@ async function m3u8Download(url, options = {}) {
185
185
  remainingTime: 0,
186
186
  localM3u8: (0, local_play_js_1.toLocalM3u8)(m3u8Info.data).replace(options.cacheDir, '').replaceAll(node_path_1.sep, '/').slice(1),
187
187
  filename: options.filename,
188
+ threadNum: options.threadNum,
188
189
  };
189
190
  const runTask = (data) => {
190
191
  for (const info of data) {
@@ -234,6 +235,7 @@ async function m3u8Download(url, options = {}) {
234
235
  if (stats.speed > stats.avgSpeed)
235
236
  stats.remainingTime = stats.remainingTime * (stats.avgSpeed / stats.speed);
236
237
  stats.remainingTime = Math.ceil(stats.remainingTime);
238
+ stats.size = stats.downloadedSize * (stats.duration / stats.durationDownloaded);
237
239
  }
238
240
  if (options.showProgress) {
239
241
  const processBar = '='.repeat(Math.floor(stats.progress * 0.2)).padEnd(20, '-');
@@ -110,11 +110,10 @@ class DLServer {
110
110
  item.status = 'pause';
111
111
  }
112
112
  else {
113
- const isError = item.status === 'done' && item.options?.convert && (!item.localVideo || !(0, node_fs_1.existsSync)(item.localVideo));
113
+ const isError = item.status === 'done' && item.options?.convert !== false && (!item.localVideo || !(0, node_fs_1.existsSync)(item.localVideo));
114
114
  if (isError) {
115
115
  item.status = 'error';
116
116
  item.errmsg = '本地视频文件不存在';
117
- item.localVideo = '';
118
117
  }
119
118
  }
120
119
  this.dlCache.set(url, item);
@@ -83,6 +83,8 @@ export interface M3u8DLProgressStats {
83
83
  localVideo?: string;
84
84
  /** 本地保存的文件名 */
85
85
  filename?: string;
86
+ /** 并发下载线程数 */
87
+ threadNum?: number;
86
88
  /** 最新的错误信息 */
87
89
  errmsg?: string;
88
90
  }
package/client/index.html CHANGED
@@ -389,6 +389,7 @@ services:
389
389
  <option value="resume">下载中</option>
390
390
  <option value="pending">等待中</option>
391
391
  <option value="pause">已暂停</option>
392
+ <option value="error">异常</option>
392
393
  <option value="done">已完成</option>
393
394
  </select>
394
395
  <button @click="clearFilters" class="px-3 py-2 text-gray-600 hover:text-gray-800" title="清除筛选条件" aria-label="清除筛选条件">
@@ -859,7 +860,7 @@ services:
859
860
  if (typeof urls === 'string') urls = [urls];
860
861
  const r = await T.post(`/pause`, { urls, all: urls[0] === 'all' });
861
862
  if (!r.code) T.toast(r.message || '已暂停下载');
862
- this.tasks[url].status = 'pause';
863
+ if (urls === this.selectedTasks) this.selectedTasks = [];
863
864
  },
864
865
  /** 恢复下载 */
865
866
  resumeDownload: async function (urls) {
@@ -867,6 +868,60 @@ services:
867
868
  if (typeof urls === 'string') urls = [urls];
868
869
  const r = await T.post(`/resume`, { urls, all: urls[0] === 'all' });
869
870
  if (!r.code) T.toast(r.message || '已恢复下载');
871
+ if (urls === this.selectedTasks) this.selectedTasks = [];
872
+ },
873
+ /** 删除选中的任务 */
874
+ async deleteDownload(urls = this.selectedTasks) {
875
+ if (!urls.length) return;
876
+ try {
877
+ const result = await Swal.fire({
878
+ title: '确认删除',
879
+ html: `
880
+ <div class="text-left">
881
+ <div class="mb-4">
882
+ <label class="inline-flex items-center">
883
+ <input type="checkbox" id="deleteCache" class="form-checkbox h-5 w-5 text-blue-500 rounded focus:ring-blue-500" checked>
884
+ <span class="ml-2 text-gray-700">同时删除已下载的缓存</span>
885
+ </label>
886
+ </div>
887
+ <div>
888
+ <label class="inline-flex items-center">
889
+ <input type="checkbox" id="deleteVideo" class="form-checkbox h-5 w-5 text-blue-500 rounded focus:ring-blue-500" checked>
890
+ <span class="ml-2 text-red-700">同时删除已下载的视频</span>
891
+ </label>
892
+ </div>
893
+ </div>
894
+ `,
895
+ showCancelButton: true,
896
+ confirmButtonText: '确认删除',
897
+ cancelButtonText: '取消',
898
+ confirmButtonColor: '#ef4444',
899
+ focusConfirm: false,
900
+ preConfirm: () => {
901
+ return {
902
+ deleteCache: document.getElementById('deleteCache').checked,
903
+ deleteVideo: document.getElementById('deleteVideo').checked
904
+ };
905
+ }
906
+ });
907
+
908
+ if (result.isConfirmed) {
909
+ const r = await T.post(`/delete`, {
910
+ urls,
911
+ deleteCache: result.value.deleteCache,
912
+ deleteVideo: result.value.deleteVideo
913
+ });
914
+ if (!r.code) {
915
+ T.toast(r.message || '已删除选中的下载');
916
+ urls.forEach(url => (delete this.tasks[url]));
917
+ if (urls === this.selectedTasks) this.selectedTasks = [];
918
+ this.forceUpdate();
919
+ }
920
+ }
921
+ } catch (error) {
922
+ console.error('删除下载失败:', error);
923
+ T.alert('删除下载失败: ' + error.message);
924
+ }
870
925
  },
871
926
  getTasks: async function () {
872
927
  this.tasks = await T.get('/tasks');
@@ -875,17 +930,17 @@ services:
875
930
  localPlay: function (task) {
876
931
  const url = location.origin + '/localplay/' + encodeURIComponent(task.localVideo || task.localM3u8);
877
932
  console.log(task);
878
- if (task.localVideo) {
879
- Swal.fire({
880
- title: task?.options.filename || task.url,
881
- width: '1000px',
882
- showCloseButton: true,
883
- showConfirmButton: false,
884
- html: `<video autoplay width="100%" style="max-width: 1000px; min-height: 300px; max-height: 90vh" src="${url}" controls></video> `,
885
- });
886
- } else {
887
- window.open(`http://lzw.me/x/m3u8-player/?url=${encodeURIComponent(url)}`);
888
- }
933
+ // return window.open(`./play.html?url=${encodeURIComponent(url)}`);
934
+ Swal.fire({
935
+ title: task?.options.filename || task.url,
936
+ width: '1000px',
937
+ padding: 0,
938
+ allowOutsideClick: false,
939
+ showCloseButton: true,
940
+ showConfirmButton: false,
941
+ html: `<iframe src="./play.html?url=${encodeURIComponent(url)}" style="width: 100%; height: 550px; max-width: 1000px; max-height: 90vh" frameborder="0" allowfullscreen></iframe>`,
942
+ // html: `<video autoplay width="100%" style="max-width: 1000px; min-height: 300px; max-height: 90vh" src="${url}" controls></video> `,
943
+ });
889
944
  },
890
945
  // 预览
891
946
  preview: function (url) {
@@ -951,59 +1006,6 @@ services:
951
1006
  this.selectedTasks.splice(index, 1);
952
1007
  }
953
1008
  },
954
- /** 删除选中的任务 */
955
- async deleteDownload(urls = this.selectedTasks) {
956
- if (!urls.length) return;
957
- try {
958
- const result = await Swal.fire({
959
- title: '确认删除',
960
- html: `
961
- <div class="text-left">
962
- <div class="mb-4">
963
- <label class="inline-flex items-center">
964
- <input type="checkbox" id="deleteCache" class="form-checkbox h-5 w-5 text-blue-500 rounded focus:ring-blue-500" checked>
965
- <span class="ml-2 text-gray-700">同时删除已下载的缓存</span>
966
- </label>
967
- </div>
968
- <div>
969
- <label class="inline-flex items-center">
970
- <input type="checkbox" id="deleteVideo" class="form-checkbox h-5 w-5 text-blue-500 rounded focus:ring-blue-500" checked>
971
- <span class="ml-2 text-red-700">同时删除已下载的视频</span>
972
- </label>
973
- </div>
974
- </div>
975
- `,
976
- showCancelButton: true,
977
- confirmButtonText: '确认删除',
978
- cancelButtonText: '取消',
979
- confirmButtonColor: '#ef4444',
980
- focusConfirm: false,
981
- preConfirm: () => {
982
- return {
983
- deleteCache: document.getElementById('deleteCache').checked,
984
- deleteVideo: document.getElementById('deleteVideo').checked
985
- };
986
- }
987
- });
988
-
989
- if (result.isConfirmed) {
990
- const r = await T.post(`/delete`, {
991
- urls,
992
- deleteCache: result.value.deleteCache,
993
- deleteVideo: result.value.deleteVideo
994
- });
995
- if (!r.code) {
996
- T.toast(r.message || '已删除选中的下载');
997
- urls.forEach(url => (delete this.tasks[url]));
998
- this.selectedTasks = [];
999
- this.forceUpdate();
1000
- }
1001
- }
1002
- } catch (error) {
1003
- console.error('删除下载失败:', error);
1004
- T.alert('删除下载失败: ' + error.message);
1005
- }
1006
- },
1007
1009
  },
1008
1010
  mounted() {
1009
1011
  T.reqHeaders.authorization = this.token ? `${this.token}` : '';
@@ -0,0 +1,44 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+
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>
15
+ </head>
16
+
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>
23
+
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>
42
+ </body>
43
+
44
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzwme/m3u8-dl",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
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",