mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-31 08:08:19 +08:00
refa: download video (#1737)
* opt: save pb danmaku * refa: download video * opt: replaceAll * fix: wait delete * opt: remove completer * fix: index.json * tweaks Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> --------- Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
committed by
GitHub
parent
37b1228552
commit
407b31c5c1
@@ -1,4 +1,4 @@
|
||||
import 'dart:async' show Completer, StreamSubscription;
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:PiliPlus/http/init.dart';
|
||||
@@ -9,186 +9,97 @@ import 'package:dio/dio.dart';
|
||||
class DownloadManager {
|
||||
final String url;
|
||||
final String path;
|
||||
final Function({required int progress, required int total}) onTaskRunning;
|
||||
final Function() onTaskComplete;
|
||||
final Function({
|
||||
required int progress,
|
||||
required int total,
|
||||
required Object error,
|
||||
})
|
||||
onTaskError;
|
||||
final void Function(int, int)? onReceiveProgress;
|
||||
final void Function([Object? error]) onDone;
|
||||
|
||||
bool _closed = false;
|
||||
DownloadStatus _status = DownloadStatus.wait;
|
||||
DownloadStatus _status = DownloadStatus.downloading;
|
||||
DownloadStatus get status => _status;
|
||||
CancelToken? _cancelToken;
|
||||
Completer? _completer;
|
||||
final _cancelToken = CancelToken();
|
||||
late Future<void> task;
|
||||
|
||||
DownloadManager({
|
||||
required this.url,
|
||||
required this.path,
|
||||
required this.onTaskRunning,
|
||||
required this.onTaskComplete,
|
||||
required this.onTaskError,
|
||||
});
|
||||
|
||||
void _complete() {
|
||||
if (_completer?.isCompleted == false) {
|
||||
_completer?.complete();
|
||||
}
|
||||
required this.onReceiveProgress,
|
||||
required this.onDone,
|
||||
}) {
|
||||
task = _start();
|
||||
}
|
||||
|
||||
Future<void> start() async {
|
||||
_completer = Completer();
|
||||
_cancelToken = CancelToken();
|
||||
_status = DownloadStatus.downloading;
|
||||
Future<void> _start() async {
|
||||
int received;
|
||||
|
||||
final file = File(path);
|
||||
// If the file already exists, the method fails.
|
||||
if (!file.existsSync()) {
|
||||
if (file.existsSync()) {
|
||||
received = await file.length();
|
||||
} else {
|
||||
file.createSync(recursive: true);
|
||||
received = 0;
|
||||
}
|
||||
|
||||
final int downloadedSize = await file.length();
|
||||
|
||||
// Shouldn't call file.writeAsBytesSync(list, flush: flush),
|
||||
// because it can write all bytes by once. Consider that the file is
|
||||
// a very big size (up to 1 Gigabytes), it will be expensive in memory.
|
||||
RandomAccessFile raf = file.openSync(
|
||||
mode: downloadedSize == 0 ? FileMode.write : FileMode.append,
|
||||
final sink = file.openWrite(
|
||||
mode: received == 0 ? FileMode.writeOnly : FileMode.writeOnlyAppend,
|
||||
);
|
||||
|
||||
Future<void>? asyncWrite;
|
||||
Future<void> closeAndDelete({bool delete = false}) async {
|
||||
if (!_closed) {
|
||||
_closed = true;
|
||||
await asyncWrite;
|
||||
await raf.close().catchError((_) => raf);
|
||||
Future<void> onError(Object e, {bool delete = false}) async {
|
||||
try {
|
||||
await sink.close();
|
||||
} catch (_) {}
|
||||
if (_status == DownloadStatus.downloading) {
|
||||
_status = DownloadStatus.failDownload;
|
||||
if (delete && file.existsSync()) {
|
||||
await file.delete().catchError((_) => file);
|
||||
await file.tryDel();
|
||||
}
|
||||
}
|
||||
onDone(e);
|
||||
}
|
||||
|
||||
final Response<ResponseBody> response;
|
||||
Response<ResponseBody> response;
|
||||
try {
|
||||
response = await Request.dio.get<ResponseBody>(
|
||||
url.http2https,
|
||||
options: Options(
|
||||
headers: {'range': 'bytes=$downloadedSize-'},
|
||||
headers: {'range': 'bytes=$received-'},
|
||||
responseType: ResponseType.stream,
|
||||
validateStatus: (status) {
|
||||
return status == 416 ||
|
||||
(status != null && status >= 200 && status < 300);
|
||||
},
|
||||
validateStatus: (status) =>
|
||||
status != null &&
|
||||
(status == 416 || (status >= 200 && status < 300)),
|
||||
),
|
||||
cancelToken: _cancelToken,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
final isFailed = e.response?.statusCode != 416;
|
||||
if (isFailed) {
|
||||
_status = DownloadStatus.failDownload;
|
||||
onTaskError(progress: 0, total: 0, error: e);
|
||||
} else {
|
||||
_status = DownloadStatus.completed;
|
||||
onTaskComplete();
|
||||
}
|
||||
closeAndDelete(delete: isFailed).whenComplete(_complete);
|
||||
await onError(e, delete: true);
|
||||
return;
|
||||
}
|
||||
final data = response.data!;
|
||||
final contentLength = data.contentLength + received;
|
||||
|
||||
int received = downloadedSize;
|
||||
|
||||
// Stream<Uint8List>
|
||||
final stream = response.data!.stream;
|
||||
|
||||
final total =
|
||||
int.parse(response.headers.value(Headers.contentLengthHeader) ?? '0') +
|
||||
downloadedSize;
|
||||
|
||||
if (downloadedSize == 0) {
|
||||
onTaskRunning(progress: 0, total: total);
|
||||
if (received == 0) {
|
||||
onReceiveProgress?.call(0, contentLength);
|
||||
}
|
||||
|
||||
late StreamSubscription subscription;
|
||||
subscription = stream.listen(
|
||||
(data) {
|
||||
subscription.pause();
|
||||
// Write file asynchronously
|
||||
asyncWrite = raf
|
||||
.writeFrom(data)
|
||||
.then((result) async {
|
||||
// Notify progress
|
||||
received += data.length;
|
||||
onTaskRunning(progress: received, total: total);
|
||||
|
||||
raf = result;
|
||||
if (_cancelToken != null && !_cancelToken!.isCancelled) {
|
||||
subscription.resume();
|
||||
}
|
||||
})
|
||||
.catchError((Object e) async {
|
||||
try {
|
||||
await subscription.cancel().catchError((_) {});
|
||||
_closed = true;
|
||||
await raf.close().catchError((_) => raf);
|
||||
if (file.existsSync()) {
|
||||
await file.delete().catchError((_) => file);
|
||||
}
|
||||
} catch (e) {
|
||||
_status = DownloadStatus.failDownload;
|
||||
onTaskError(progress: received, total: total, error: e);
|
||||
} finally {
|
||||
_complete();
|
||||
}
|
||||
});
|
||||
},
|
||||
onDone: () async {
|
||||
try {
|
||||
await asyncWrite;
|
||||
_closed = true;
|
||||
await raf.close().catchError((_) => raf);
|
||||
_status = DownloadStatus.completed;
|
||||
onTaskComplete();
|
||||
} catch (e) {
|
||||
_status = DownloadStatus.failDownload;
|
||||
onTaskError(progress: received, total: total, error: e);
|
||||
} finally {
|
||||
_complete();
|
||||
}
|
||||
},
|
||||
onError: (e) async {
|
||||
try {
|
||||
await closeAndDelete(delete: true);
|
||||
} catch (e) {
|
||||
_cancel();
|
||||
_status = DownloadStatus.failDownload;
|
||||
onTaskError(progress: received, total: total, error: e);
|
||||
} finally {
|
||||
_complete();
|
||||
}
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
_cancelToken?.whenCancel.then((_) async {
|
||||
await subscription.cancel();
|
||||
await closeAndDelete();
|
||||
_complete();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void>? _cancel() {
|
||||
if (_cancelToken != null) {
|
||||
_cancelToken?.cancel();
|
||||
_cancelToken = null;
|
||||
try {
|
||||
await for (final chunk in data.stream) {
|
||||
sink.add(chunk);
|
||||
received += chunk.length;
|
||||
onReceiveProgress?.call(received, contentLength);
|
||||
}
|
||||
await sink.close();
|
||||
_status = DownloadStatus.completed;
|
||||
onDone();
|
||||
} catch (e) {
|
||||
await onError(e);
|
||||
return;
|
||||
}
|
||||
return _completer?.future;
|
||||
}
|
||||
|
||||
Future<void>? cancel({required bool isDelete}) {
|
||||
Future<void> cancel({required bool isDelete}) {
|
||||
if (!isDelete && _status == DownloadStatus.downloading) {
|
||||
_status = DownloadStatus.pause;
|
||||
}
|
||||
return _cancel();
|
||||
if (!_cancelToken.isCancelled) {
|
||||
_cancelToken.cancel();
|
||||
}
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user