11import 'dart:io' ;
22import 'package:dio/dio.dart' ;
33import 'package:file_picker/file_picker.dart' ;
4+ import 'package:kazumi/utils/m3u8_parser.dart' ;
45import 'package:path/path.dart' as path;
56
6- /// 测试用例:《弹珠汽水瓶里的千岁同学》
7+ /// 测试用例:
78/// [kazumi webview parser] : (7sefun) => 可工作
89/// Loading video source: https://v16-tiktokcdn-com.akamaized.net/1a4e3d45db24b04b9792187b64763d9a/69084faf/video/tos/alisg/tos-alisg-ve-0051c001-sg/oIu0QFDTNDBfbIPDIlEQTuBA2lSgglEY8ofwI3/?a=1233&bti=Nzg3NWYzLTQ6&ch=0&cr=0&dr=0&er=0&lr=default&cd=0%7C0%7C0%7C0&br=1870&bt=935&cs=0&ds=4&ft=.bvrXInz7ThFo_pPXq8Zmo&mime_type=video_mp4&qs=0&rc=ZztlODhoZjxoaDc0aDc6OkBpMzdqNWw5cjNoNjMzODYzNEAzLzJjXmFfNl4xYjVgY2MyYSNgZmFxMmRza21hLS1kMC1zcw%3D%3D&vvpl=1&l=20251102142227CD49B547C5785EB959BB&btag=e000a8000&vid=v10033g50000d3pen0vog65it2dgi4c0
910/// [kazumi webview parser] : (DM84)
10- /// Loading m3u8 source: https://vip.dytt-cinema.com/20251029/38765_b0889565 /index.m3u8
11+ /// Loading m3u8 source: https://vip.dytt-cinema.com/20251106/39808_a9a80dd4 /index.m3u8
1112/// xfdm 暂时没找到合适的
1213
1314class Downloads {
@@ -24,31 +25,109 @@ class Downloads {
2425 bool get isDownloadingM3u8 => _isDownloadingM3u8;
2526 bool get isDownloadingMP4 => _isDownloadingMP4;
2627
27- Future <bool > downloadM3u8 (String url) async {
28- // TODO: m3u8 可能需要额外写个解析插件,先支持 mp4 吧
28+ Future <bool > downloadAndMergeTs (String url,
29+ {String ? album, String ? fileName, bool delTemp = true }) async {
30+ if (_isDownloadingM3u8) {
31+ print ("[kazumi downloader]: 正在下载中,请稍后" );
32+ }
33+
34+ final m3u8Src = await M3U8Parser .parse (dio, url);
35+ if (m3u8Src == null ) {
36+ print ("[kazumi downloader]: 分片源列表为空,无法下载 ts 文件" );
37+ return false ;
38+ }
39+
40+ // 此处只是需要一个存储 m3u8 文件的目录
41+ final savePath = await _getSavePath (album: album);
42+ if (savePath == null ) {
43+ return false ;
44+ }
45+
46+ final tempPath = path.join (savePath, "temp" );
47+ final tsFiles = < File > [];
48+
49+ _isDownloadingM3u8 = true ;
50+ for (final segment in m3u8Src.segments) {
51+ final tsFile = File (path.join (tempPath, segment.url.split ('/' ).last));
52+ print ("[kazumi downloader]: 正在下载 ${tsFile .path }" );
53+ tsFiles.add (tsFile);
54+ if (tsFile.existsSync ()) {
55+ print ("[kazumi downloader]: 文件已存在,跳过" );
56+ continue ;
57+ }
58+ await _downloadFile (url: segment.url, savePath: tsFile.path);
59+ }
60+
61+ final outputFile = File (path.join (savePath, fileName ?? "output.ts" ));
62+ final outputSink = outputFile.openWrite ();
63+ for (final tsFile in tsFiles) {
64+ print ("[kazumi downloader]: 正在合并 ${tsFile .path }" );
65+ if (tsFile.existsSync ()) {
66+ await outputSink.addStream (tsFile.openRead ());
67+ }
68+ }
69+ await outputSink.close ();
70+
71+ if (delTemp) {
72+ for (final tsFile in tsFiles) {
73+ if (tsFile.existsSync ()) {
74+ await tsFile.delete ();
75+ }
76+ }
77+ }
78+
79+ _isDownloadingM3u8 = false ;
80+
2981 return true ;
3082 }
3183
3284 /// 下载 MP4,album 可以是番剧名(用于做下载合集),fileName 是文件名
33- Future <void > downloadMP4 (String url,
34- {String album= '' , required String fileName}) async {
85+ Future <bool > downloadMP4 (String url,
86+ {String ? album, required String fileName}) async {
3587 if (_isDownloadingMP4) {
3688 print ("[kazumi downloader]: 正在下载中,请稍后" );
3789 }
90+
91+ final String ? savePath =
92+ await _getSavePath (album: album, fileName: fileName);
93+ if (savePath == null ) {
94+ _isDownloadingMP4 = false ;
95+ return false ;
96+ }
97+ print ("[kazumi downloader]: 保存路径为: $savePath " );
98+
3899 _isDownloadingMP4 = true ;
100+ await _downloadFile (url: url, savePath: savePath);
101+ _isDownloadingMP4 = false ;
102+
103+ return true ;
104+ }
39105
106+ Future <String ?> _getSavePath ({String ? album, String ? fileName}) async {
40107 String ? savePath = await FilePicker .platform.getDirectoryPath ();
41- final suggestedName =
42- fileName.contains (RegExp (r'\.\w+$' )) ? fileName : '$fileName .mp4' ;
43108
44109 if (savePath == null ) {
45110 print ("[kazumi downloader]: 保存动作已取消" );
46- return ;
111+ return null ;
47112 }
48- savePath = path.join (savePath, album, suggestedName);
49- print ("[kazumi downloader]: 保存路径为: $savePath " );
113+ if (album != null ) {
114+ savePath = path.join (savePath, album);
115+ }
116+ if (fileName != null ) {
117+ savePath = path.join (savePath, fileName);
118+ }
119+
120+ return savePath;
121+ }
122+
123+ Future <void > _downloadFile ({
124+ required String url,
125+ required String savePath,
126+ }) async {
50127 try {
51- await dio.download (url, savePath, onReceiveProgress: (received, total) {
128+ await dio.download (url, savePath,
129+ // `onReceiveProgress` 也许是个可选的功能,缺点是它会导致调试信息过多,我不太确定是否保留:
130+ onReceiveProgress: (received, total) {
52131 if (total != - 1 ) {
53132 print (
54133 "[kazumi downloader]: 已下载 ${((received / total ) * 100 ).toStringAsFixed (1 )}%" );
@@ -60,20 +139,21 @@ class Downloads {
60139 responseType: ResponseType .bytes,
61140 ));
62141 } catch (e) {
63- print ("[kazumi downloader]: 无法获取视频字节流 ,错误为: $e " );
142+ print ("[kazumi downloader]: 无法获取文件字节流 ,错误为: $e " );
64143 // 删除不完整文件
65144 if (await File (savePath).exists ()) {
66145 await File (savePath).delete ();
67146 }
68- } finally {
69- _isDownloadingMP4 = false ;
70147 }
71148 }
72149}
150+
73151// 测试用例
74- // void test() async {
75- // await Downloads().downloadMP4(
76- // "https://v16-tiktokcdn-com.akamaized.net/1a4e3d45db24b04b9792187b64763d9a/69084faf/video/tos/alisg/tos-alisg-ve-0051c001-sg/oIu0QFDTNDBfbIPDIlEQTuBA2lSgglEY8ofwI3/?a=1233&bti=Nzg3NWYzLTQ6&ch=0&cr=0&dr=0&er=0&lr=default&cd=0%7C0%7C0%7C0&br=1870&bt=935&cs=0&ds=4&ft=.bvrXInz7ThFo_pPXq8Zmo&mime_type=video_mp4&qs=0&rc=ZztlODhoZjxoaDc0aDc6OkBpMzdqNWw5cjNoNjMzODYzNEAzLzJjXmFfNl4xYjVgY2MyYSNgZmFxMmRza21hLS1kMC1zcw%3D%3D&vvpl=1&l=20251102142227CD49B547C5785EB959BB&btag=e000a8000&vid=v10033g50000d3pen0vog65it2dgi4c0",
77- // fileName: "test.mp4",
78- // );
79- // }
152+ void test () async {
153+ // await Downloads().downloadMP4(
154+ // "https://v16-tiktokcdn-com.akamaized.net/1a4e3d45db24b04b9792187b64763d9a/69084faf/video/tos/alisg/tos-alisg-ve-0051c001-sg/oIu0QFDTNDBfbIPDIlEQTuBA2lSgglEY8ofwI3/?a=1233&bti=Nzg3NWYzLTQ6&ch=0&cr=0&dr=0&er=0&lr=default&cd=0%7C0%7C0%7C0&br=1870&bt=935&cs=0&ds=4&ft=.bvrXInz7ThFo_pPXq8Zmo&mime_type=video_mp4&qs=0&rc=ZztlODhoZjxoaDc0aDc6OkBpMzdqNWw5cjNoNjMzODYzNEAzLzJjXmFfNl4xYjVgY2MyYSNgZmFxMmRza21hLS1kMC1zcw%3D%3D&vvpl=1&l=20251102142227CD49B547C5785EB959BB&btag=e000a8000&vid=v10033g50000d3pen0vog65it2dgi4c0",
155+ // fileName: "test.mp4",
156+ // );
157+ await Downloads ().downloadAndMergeTs (
158+ "https://vip.dytt-cinema.com/20251029/38765_b0889565/index.m3u8" );
159+ }
0 commit comments