@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.
- package/cjs/lib/m3u8-convert.js +6 -4
- package/cjs/lib/m3u8-download.js +2 -0
- package/cjs/server/download-server.js +1 -2
- package/cjs/types/m3u8.d.ts +2 -0
- package/client/index.html +67 -65
- package/client/play.html +44 -0
- package/package.json +1 -1
package/cjs/lib/m3u8-convert.js
CHANGED
|
@@ -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.
|
|
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\
|
|
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
|
-
|
|
38
|
+
else
|
|
39
|
+
(0, node_fs_1.unlinkSync)(ffconcatFile);
|
|
38
40
|
}
|
|
39
41
|
if (!ffmpegSupport) {
|
|
40
42
|
filepath = filepath.replace(/\.mp4$/, '.ts');
|
package/cjs/lib/m3u8-download.js
CHANGED
|
@@ -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);
|
package/cjs/types/m3u8.d.ts
CHANGED
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.
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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}` : '';
|
package/client/play.html
ADDED
|
@@ -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>
|