This commit is contained in:
Alessandro Autiero
2024-06-02 15:12:42 +02:00
parent efb508bd0c
commit 5d89a603d7
63 changed files with 1146 additions and 1379 deletions

View File

@@ -8,6 +8,7 @@ import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'package:reboot_common/src/extension/types.dart';
const String kStopBuildDownloadSignal = "kill";
@@ -23,61 +24,14 @@ Dio _buildDioInstance() {
return dio;
}
final String _archiveSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md";
final String _archiveSourceUrl = "http://185.203.216.3/versions.json";
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
const String _manifestSourceUrl = "http://manifest.simplyblk.xyz";
const String _deniedConnectionError = "The connection was denied: your firewall might be blocking the download";
const String _unavailableError = "The build downloader is not available right now";
const String _genericError = "The build downloader is not working correctly";
const int _maxErrors = 100;
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
(_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () =>
HttpClient()
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
final results = await Future.wait([_fetchManifestBuilds(), _fetchArchiveBuilds()]);
final data = <FortniteBuild>[];
for(final result in results) {
data.addAll(result);
}
return data;
}
Future<List<FortniteBuild>> _fetchManifestBuilds() async {
try {
final response = await _dio.get<String>(
"$_manifestSourceUrl/versions.json",
options: Options(
headers: {
"Accept-Encoding": "*",
"Cookie": "_c_t_c=1"
}
)
);
final body = response.data;
return jsonDecode(body!).map((version) {
final nameParts = version.split("-");
if(nameParts.length < 2) {
return null;
}
final name = nameParts[1];
return FortniteBuild(
identifier: name,
version: "Fortnite ${name}",
link: "$_manifestSourceUrl/$name/$name.manifest",
source: FortniteBuildSource.manifest
);
}).whereType<FortniteBuild>().toList();
}catch(_) {
return [];
}
}
Future<List<FortniteBuild>> _fetchArchiveBuilds() async {
final response = await _dio.get<String>(
_archiveSourceUrl,
options: Options(
@@ -88,32 +42,15 @@ Future<List<FortniteBuild>> _fetchArchiveBuilds() async {
return [];
}
final data = jsonDecode(response.data ?? "{}");
var results = <FortniteBuild>[];
for (var line in response.data?.split("\n") ?? []) {
if(!line.startsWith("|")) {
continue;
}
var parts = line.substring(1, line.length - 1).split("|");
if(parts.isEmpty) {
continue;
}
var link = parts.last.trim();
if(!link.endsWith(".zip") && !link.endsWith(".rar")) {
continue;
}
var version = parts.first.trim();
version = version.substring(0, version.indexOf("-"));
for(final entry in data.entries) {
results.add(FortniteBuild(
identifier: version,
version: "Fortnite $version",
link: link,
source: FortniteBuildSource.archive
identifier: entry.key,
version: "${entry.value["title"]} (${entry.key})",
link: entry.value["url"]
));
}
return results;
}
@@ -121,107 +58,23 @@ Future<List<FortniteBuild>> _fetchArchiveBuilds() async {
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
try {
final stopped = _setupLifecycle(options);
switch(options.build.source) {
case FortniteBuildSource.archive:
final outputDir = Directory("${options.destination.path}\\.build");
await outputDir.create(recursive: true);
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
final extension = path.extension(fileName);
final tempFile = File("${outputDir.path}\\$fileName");
if(await tempFile.exists()) {
await tempFile.delete(recursive: true);
}
final startTime = DateTime.now().millisecondsSinceEpoch;
final response = _downloadArchive(options, tempFile, startTime);
await Future.any([stopped.future, response]);
if(!stopped.isCompleted) {
await _extractArchive(stopped, extension, tempFile, options);
}
delete(outputDir);
break;
case FortniteBuildSource.manifest:
final response = await _dio.get<String>(
options.build.link,
options: Options(
headers: {
"Cookie": "_c_t_c=1"
}
)
);
final manifest = FortniteBuildManifestFile.fromJson(jsonDecode(response.data!));
final totalBytes = manifest.size;
final outputDir = options.destination;
await outputDir.create(recursive: true);
final startTime = DateTime.now().millisecondsSinceEpoch;
final codec = GZipCodec();
var completedBytes = 0;
var lastPercentage = 0.0;
final writers = manifest.chunks.map((chunkedFile) async {
final outputFile = File('${outputDir.path}/${chunkedFile.file}');
if(outputFile.existsSync()) {
if(outputFile.lengthSync() != chunkedFile.fileSize) {
await outputFile.delete();
} else {
completedBytes += chunkedFile.fileSize;
final percentage = completedBytes * 100 / totalBytes;
if(percentage - lastPercentage > 0.1) {
_onProgress(
startTime,
DateTime.now().millisecondsSinceEpoch,
percentage,
false,
options
);
}
return;
}
}
await outputFile.parent.create(recursive: true);
for(final chunkId in chunkedFile.chunksIds) {
final response = await _dio.get<Uint8List>(
"$_manifestSourceUrl/${options.build.identifier}/$chunkId.chunk",
options: Options(
responseType: ResponseType.bytes,
headers: {
"Accept-Encoding": "gzip",
"Cookie": "_c_t_c=1"
}
),
);
var responseBody = response.data;
if(responseBody == null) {
continue;
}
final decodedBody = codec.decode(responseBody);
await outputFile.writeAsBytes(
decodedBody,
mode: FileMode.append,
flush: true
);
completedBytes += decodedBody.length;
final percentage = completedBytes * 100 / totalBytes;
if(percentage - lastPercentage > 0.1) {
_onProgress(
startTime,
DateTime.now().millisecondsSinceEpoch,
percentage,
false,
options
);
}
}
});
await Future.any([stopped.future, Future.wait(writers)]);
break;
final outputDir = Directory("${options.destination.path}\\.build");
await outputDir.create(recursive: true);
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
final extension = path.extension(fileName);
final tempFile = File("${outputDir.path}\\$fileName");
if(await tempFile.exists()) {
await tempFile.delete(recursive: true);
}
final startTime = DateTime.now().millisecondsSinceEpoch;
final response = _downloadArchive(options, tempFile, startTime);
await Future.any([stopped.future, response]);
if(!stopped.isCompleted) {
await _extractArchive(stopped, extension, tempFile, options);
}
delete(outputDir);
}catch(error) {
_onError(error, options);
}
@@ -358,7 +211,7 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
throw ArgumentError("Unexpected file extension: $extension}");
}
await Future.any([stopped.future, watchProcess(process.pid)]);
await Future.any([stopped.future, process.exitCode]);
}
void _onProgress(int startTime, int? now, double percentage, bool extracting, FortniteBuildDownloadOptions options) {
@@ -385,7 +238,6 @@ void _onError(Object? error, FortniteBuildDownloadOptions options) {
}
}
Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
var stopped = Completer();
var lifecyclePort = ReceivePort();