fix #176
This commit is contained in:
parent
281e621da3
commit
e2c79c829b
12 changed files with 468 additions and 48 deletions
|
|
@ -3,19 +3,17 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader.dart';
|
||||
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_anime.dart';
|
||||
|
||||
import 'database.dart';
|
||||
import 'exceptions.dart';
|
||||
import 'models.dart';
|
||||
import 'persistent_storage.dart';
|
||||
import 'queue/task_queue.dart';
|
||||
import 'task.dart';
|
||||
import 'desktop/desktop_downloader.dart';
|
||||
|
||||
/// Common download functionality
|
||||
///
|
||||
|
|
@ -94,7 +92,8 @@ abstract base class BaseDownloader {
|
|||
final canResumeTask = <Task, Completer<bool>>{};
|
||||
|
||||
/// Flag indicating we have retrieved missed data
|
||||
var _retrievedLocallyStoredData = false;
|
||||
@visibleForTesting
|
||||
var retrievedLocallyStoredData = false;
|
||||
|
||||
/// Connected TaskQueues that will receive a signal upon task completion
|
||||
final taskQueues = <TaskQueue>[];
|
||||
|
|
@ -171,7 +170,7 @@ abstract base class BaseDownloader {
|
|||
/// Retrieve data that was stored locally because it could not be
|
||||
/// delivered to the downloader
|
||||
Future<void> retrieveLocallyStoredData() async {
|
||||
if (!_retrievedLocallyStoredData) {
|
||||
if (!retrievedLocallyStoredData) {
|
||||
final resumeDataMap = await popUndeliveredData(Undelivered.resumeData);
|
||||
for (var jsonString in resumeDataMap.values) {
|
||||
final resumeData = ResumeData.fromJsonString(jsonString);
|
||||
|
|
@ -188,7 +187,7 @@ abstract base class BaseDownloader {
|
|||
for (var jsonString in progressUpdateMap.values) {
|
||||
processProgressUpdate(TaskProgressUpdate.fromJsonString(jsonString));
|
||||
}
|
||||
_retrievedLocallyStoredData = true;
|
||||
retrievedLocallyStoredData = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -196,7 +195,7 @@ abstract base class BaseDownloader {
|
|||
///
|
||||
/// Matches on task, then on group, then on default
|
||||
TaskNotificationConfig? notificationConfigForTask(Task task) {
|
||||
if (task.group == chunkGroup) {
|
||||
if (task.group == chunkGroup || task is DataTask) {
|
||||
return null;
|
||||
}
|
||||
return notificationConfigs
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:mangayomi/services/background_downloader/background_downloader.dart';
|
||||
import 'desktop_downloader.dart';
|
||||
import 'download_isolate.dart';
|
||||
import 'isolate.dart';
|
||||
|
||||
/// Do the data task
|
||||
///
|
||||
/// Sends updates via the [sendPort] and can be commanded to cancel via
|
||||
/// the [messagesToIsolate] queue
|
||||
Future<void> doDataTask(DataTask task, SendPort sendPort) async {
|
||||
final client = DesktopDownloader.httpClient;
|
||||
var request = http.Request(task.httpRequestMethod, Uri.parse(task.url));
|
||||
request.headers.addAll(task.headers);
|
||||
if (task.post is String) {
|
||||
request.body = task.post!;
|
||||
}
|
||||
var resultStatus = TaskStatus.failed;
|
||||
try {
|
||||
final response = await client.send(request);
|
||||
if (!isCanceled) {
|
||||
responseHeaders = response.headers;
|
||||
responseStatusCode = response.statusCode;
|
||||
extractContentType(response.headers);
|
||||
responseBody = await responseContent(response);
|
||||
if (okResponses.contains(response.statusCode)) {
|
||||
resultStatus = TaskStatus.complete;
|
||||
} else {
|
||||
if (response.statusCode == 404) {
|
||||
resultStatus = TaskStatus.notFound;
|
||||
} else {
|
||||
taskException = TaskHttpException(
|
||||
responseBody?.isNotEmpty == true
|
||||
? responseBody!
|
||||
: response.reasonPhrase ?? 'Invalid HTTP Request',
|
||||
response.statusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logError(task, e.toString());
|
||||
setTaskError(e);
|
||||
}
|
||||
if (isCanceled) {
|
||||
// cancellation overrides other results
|
||||
resultStatus = TaskStatus.canceled;
|
||||
}
|
||||
processStatusUpdateInIsolate(task, resultStatus, sendPort);
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import 'dart:async';
|
|||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -14,7 +13,6 @@ import 'package:logging/logging.dart';
|
|||
import 'package:mangayomi/services/http/m_client.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../base_downloader.dart';
|
||||
import '../chunk.dart';
|
||||
import '../exceptions.dart';
|
||||
|
|
@ -34,7 +32,7 @@ const okResponses = [200, 201, 202, 203, 204, 205, 206];
|
|||
final class DesktopDownloader extends BaseDownloader {
|
||||
static final _log = Logger('DesktopDownloader');
|
||||
static const unlimited = 1 << 20;
|
||||
var maxConcurrent = 20;
|
||||
var maxConcurrent = 10;
|
||||
var maxConcurrentByHost = unlimited;
|
||||
var maxConcurrentByGroup = unlimited;
|
||||
static final DesktopDownloader _singleton = DesktopDownloader._internal();
|
||||
|
|
@ -565,12 +563,12 @@ final class DesktopDownloader extends BaseDownloader {
|
|||
///
|
||||
/// This is a convenience method, bundling the [requestTimeout],
|
||||
/// [proxy] and [bypassTLSCertificateValidation]
|
||||
static Future<void> setHttpClient(Duration? requestTimeout,
|
||||
Map<String, dynamic> proxy, bool bypassTLSCertificateValidation) async {
|
||||
static void setHttpClient(Duration? requestTimeout,
|
||||
Map<String, dynamic> proxy, bool bypassTLSCertificateValidation) {
|
||||
_requestTimeout = requestTimeout;
|
||||
_proxy = proxy;
|
||||
_bypassTLSCertificateValidation = bypassTLSCertificateValidation;
|
||||
await _recreateClient();
|
||||
_recreateClient();
|
||||
}
|
||||
|
||||
/// Recreates the [httpClient] used for Requests and isolate downloads/uploads
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'dart:async';
|
|||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -13,7 +12,6 @@ import 'package:logging/logging.dart';
|
|||
import 'package:mangayomi/services/http/m_client.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../base_downloader.dart';
|
||||
import '../chunk.dart';
|
||||
import '../exceptions.dart';
|
||||
|
|
@ -33,11 +31,10 @@ const okResponses = [200, 201, 202, 203, 204, 205, 206];
|
|||
final class DesktopDownloaderAnime extends BaseDownloader {
|
||||
static final _log = Logger('DesktopDownloaderAnime');
|
||||
static const unlimited = 1 << 20;
|
||||
var maxConcurrent = 20;
|
||||
var maxConcurrent = 10;
|
||||
var maxConcurrentByHost = unlimited;
|
||||
var maxConcurrentByGroup = unlimited;
|
||||
static final DesktopDownloaderAnime _singleton =
|
||||
DesktopDownloaderAnime._internal();
|
||||
static final DesktopDownloaderAnime _singleton = DesktopDownloaderAnime._internal();
|
||||
final _queue = PriorityQueue<Task>();
|
||||
final _running = Queue<Task>(); // subset that is running
|
||||
final _resume = <Task>{};
|
||||
|
|
@ -565,12 +562,12 @@ final class DesktopDownloaderAnime extends BaseDownloader {
|
|||
///
|
||||
/// This is a convenience method, bundling the [requestTimeout],
|
||||
/// [proxy] and [bypassTLSCertificateValidation]
|
||||
static Future<void> setHttpClient(Duration? requestTimeout,
|
||||
Map<String, dynamic> proxy, bool bypassTLSCertificateValidation) async {
|
||||
static void setHttpClient(Duration? requestTimeout,
|
||||
Map<String, dynamic> proxy, bool bypassTLSCertificateValidation) {
|
||||
_requestTimeout = requestTimeout;
|
||||
_proxy = proxy;
|
||||
_bypassTLSCertificateValidation = bypassTLSCertificateValidation;
|
||||
await _recreateClient();
|
||||
_recreateClient();
|
||||
}
|
||||
|
||||
/// Recreates the [httpClient] used for Requests and isolate downloads/uploads
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ import 'dart:isolate';
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:mangayomi/services/background_downloader/src/exceptions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:mangayomi/services/background_downloader/background_downloader.dart';
|
||||
|
||||
import '../models.dart';
|
||||
import '../task.dart';
|
||||
import 'data_isolate.dart';
|
||||
import 'desktop_downloader.dart';
|
||||
import 'download_isolate.dart';
|
||||
import 'parallel_download_isolate.dart';
|
||||
|
|
@ -62,7 +65,7 @@ Future<void> doTask((RootIsolateToken, SendPort) isolateArguments) async {
|
|||
bool isResume,
|
||||
Duration? requestTimeout,
|
||||
Map<String, dynamic> proxy,
|
||||
bool bypassTLSCertificateValidation,
|
||||
bool bypassTLSCertificateValidation
|
||||
) = await messagesToIsolate.next;
|
||||
DesktopDownloader.setHttpClient(
|
||||
requestTimeout, proxy, bypassTLSCertificateValidation);
|
||||
|
|
@ -95,7 +98,8 @@ Future<void> doTask((RootIsolateToken, SendPort) isolateArguments) async {
|
|||
sendPort),
|
||||
DownloadTask() => doDownloadTask(task, filePath, resumeData, isResume,
|
||||
requestTimeout ?? const Duration(seconds: 60), sendPort),
|
||||
UploadTask() => doUploadTask(task, filePath, sendPort)
|
||||
UploadTask() => doUploadTask(task, filePath, sendPort),
|
||||
DataTask() => doDataTask(task, sendPort)
|
||||
};
|
||||
}
|
||||
receivePort.close();
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import 'dart:convert';
|
|||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mangayomi/services/background_downloader/src/exceptions.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../models.dart';
|
||||
|
|
@ -43,7 +43,6 @@ Future<TaskStatus> binaryUpload(
|
|||
return TaskStatus.failed;
|
||||
}
|
||||
final fileSize = inFile.lengthSync();
|
||||
// determine the content length of the multi-part data
|
||||
var resultStatus = TaskStatus.failed;
|
||||
try {
|
||||
final client = DesktopDownloader.httpClient;
|
||||
|
|
@ -52,6 +51,8 @@ Future<TaskStatus> binaryUpload(
|
|||
request.headers.addAll(task.headers);
|
||||
request.contentLength = fileSize;
|
||||
request.headers['Content-Type'] = task.mimeType;
|
||||
request.headers['Content-Disposition'] =
|
||||
'attachment; filename="${task.filename}"';
|
||||
// initiate the request and handle completion async
|
||||
final requestCompleter = Completer();
|
||||
var transferBytesResult = TaskStatus.failed;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,17 @@
|
|||
// ignore_for_file: depend_on_referenced_packages
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:mangayomi/services/background_downloader/background_downloader.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:mangayomi/services/background_downloader/background_downloader.dart';
|
||||
|
||||
import 'base_downloader.dart';
|
||||
import 'localstore/localstore.dart';
|
||||
import 'desktop/desktop_downloader.dart';
|
||||
|
||||
/// Provides access to all functions of the plugin in a single place.
|
||||
interface class FileDownloader {
|
||||
static FileDownloader? _singleton;
|
||||
|
||||
/// If no group is specified the default group name will be used
|
||||
static const defaultGroup = 'default';
|
||||
|
||||
|
|
@ -36,8 +33,13 @@ interface class FileDownloader {
|
|||
|
||||
factory FileDownloader(
|
||||
{PersistentStorage? persistentStorage, bool isAnime = false}) {
|
||||
return FileDownloader._internal(
|
||||
assert(
|
||||
_singleton == null || persistentStorage == null,
|
||||
'You can only supply a persistentStorage on the very first call to '
|
||||
'FileDownloader()');
|
||||
_singleton ??= FileDownloader._internal(
|
||||
persistentStorage ?? LocalStorePersistentStorage(), isAnime);
|
||||
return _singleton!;
|
||||
}
|
||||
|
||||
FileDownloader._internal(PersistentStorage persistentStorage, bool isAnime) {
|
||||
|
|
@ -271,6 +273,32 @@ interface class FileDownloader {
|
|||
onElapsedTime: onElapsedTime,
|
||||
elapsedTimeInterval: elapsedTimeInterval);
|
||||
|
||||
/// Transmit data in the [DataTask] and receive the response
|
||||
///
|
||||
/// Different from [enqueue], this method returns a [Future] that completes
|
||||
/// when the [DataTask] has completed, or an error has occurred.
|
||||
/// While it uses the same mechanism as [enqueue],
|
||||
/// and will execute the task also when
|
||||
/// the app moves to the background, it is meant for data tasks that are
|
||||
/// awaited while the app is in the foreground.
|
||||
///
|
||||
/// [onStatus] is an optional callback for status updates
|
||||
///
|
||||
/// An optional callback [onElapsedTime] will be called at regular intervals
|
||||
/// (defined by [elapsedTimeInterval], which defaults to 5 seconds) with a
|
||||
/// single argument that is the elapsed time since the call to [transmit].
|
||||
/// This can be used to trigger UI warnings (e.g. 'this is taking rather long')
|
||||
/// For performance reasons the [elapsedTimeInterval] should not be set to
|
||||
/// a value less than one second.
|
||||
Future<TaskStatusUpdate> transmit(DataTask task,
|
||||
{void Function(TaskStatus)? onStatus,
|
||||
void Function(Duration)? onElapsedTime,
|
||||
Duration? elapsedTimeInterval}) =>
|
||||
_downloader.enqueueAndAwait(task,
|
||||
onStatus: onStatus,
|
||||
onElapsedTime: onElapsedTime,
|
||||
elapsedTimeInterval: elapsedTimeInterval);
|
||||
|
||||
/// Enqueues a list of files to download and returns when all downloads
|
||||
/// have finished (successfully or otherwise). The returned value is a
|
||||
/// [Batch] object that contains the original [tasks], the
|
||||
|
|
@ -864,13 +892,7 @@ Future<http.Response> _doRequest(
|
|||
(Request, Duration?, Map<String, dynamic>, bool) params) async {
|
||||
final (request, requestTimeout, proxy, bypassTLSCertificateValidation) =
|
||||
params;
|
||||
Logger.root.level = Level.ALL;
|
||||
Logger.root.onRecord.listen((LogRecord rec) {
|
||||
if (kDebugMode) {
|
||||
print('${rec.loggerName}>${rec.level.name}: ${rec.time}: ${rec.message}');
|
||||
}
|
||||
});
|
||||
final log = Logger('FileDownloader.request');
|
||||
|
||||
DesktopDownloader.setHttpClient(
|
||||
requestTimeout, proxy, bypassTLSCertificateValidation);
|
||||
final client = DesktopDownloader.httpClient;
|
||||
|
|
@ -895,7 +917,6 @@ Future<http.Response> _doRequest(
|
|||
return response;
|
||||
}
|
||||
} catch (e) {
|
||||
log.warning(e);
|
||||
response = http.Response('', 499, reasonPhrase: e.toString());
|
||||
}
|
||||
// error, retry if allowed
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import 'package:path/path.dart' as p;
|
|||
import 'package:path_provider/path_provider.dart';
|
||||
import 'dart:math';
|
||||
|
||||
import 'utils/io.dart';
|
||||
import 'utils/html.dart' if (dart.library.io) 'utils/io.dart';
|
||||
|
||||
part 'collection_ref.dart';
|
||||
part 'collection_ref_impl.dart';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
// ignore: avoid_web_libraries_in_flutter
|
||||
import 'dart:html' as html;
|
||||
|
||||
import 'utils_impl.dart';
|
||||
|
||||
/// Utils class
|
||||
class Utils implements UtilsImpl {
|
||||
Utils._();
|
||||
static final Utils _utils = Utils._();
|
||||
static Utils get instance => _utils;
|
||||
|
||||
@override
|
||||
void clearCache() {
|
||||
// no cache on web
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>?> get(String path,
|
||||
[bool? isCollection = false, List<List>? conditions]) async {
|
||||
// Fetch the documents for this collection
|
||||
if (isCollection != null && isCollection == true) {
|
||||
var dataCol = html.window.localStorage.entries.singleWhere(
|
||||
(e) => e.key == path,
|
||||
orElse: () => const MapEntry('', ''),
|
||||
);
|
||||
if (dataCol.key != '') {
|
||||
if (conditions != null && conditions.first.isNotEmpty) {
|
||||
return _getAll(dataCol);
|
||||
/*
|
||||
final ck = conditions.first[0] as String;
|
||||
final co = conditions.first[1];
|
||||
final cv = conditions.first[2];
|
||||
// With conditions
|
||||
try {
|
||||
final mapCol = json.decode(dataCol.value) as Map<String, dynamic>;
|
||||
final its = SplayTreeMap.of(mapCol);
|
||||
its.removeWhere((key, value) {
|
||||
if (value is Map<String, dynamic>) {
|
||||
final key = value.keys.contains(ck);
|
||||
final check = value[ck] as bool;
|
||||
return !(key == true && check == cv);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
its.forEach((key, value) {
|
||||
final data = value as Map<String, dynamic>;
|
||||
_data[key] = data;
|
||||
});
|
||||
return _data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
*/
|
||||
} else {
|
||||
return _getAll(dataCol);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final data = await _readFromStorage(path);
|
||||
final id = path.substring(path.lastIndexOf('/') + 1, path.length);
|
||||
if (data is Map<String, dynamic>) {
|
||||
if (data.containsKey(id)) return data[id];
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<dynamic>? set(Map<String, dynamic> data, String path) {
|
||||
return _writeToStorage(data, path);
|
||||
}
|
||||
|
||||
@override
|
||||
Future delete(String path) async {
|
||||
_deleteFromStorage(path);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Map<String, dynamic>> stream(String path, [List<List>? conditions]) {
|
||||
// ignore: close_sinks
|
||||
final storage = _storageCache[path] ??
|
||||
_storageCache.putIfAbsent(
|
||||
path, () => StreamController<Map<String, dynamic>>.broadcast());
|
||||
|
||||
_initStream(storage, path);
|
||||
return storage.stream;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _getAll(MapEntry<String, String> dataCol) {
|
||||
final items = <String, dynamic>{};
|
||||
try {
|
||||
final mapCol = json.decode(dataCol.value) as Map<String, dynamic>;
|
||||
mapCol.forEach((key, value) {
|
||||
final data = value as Map<String, dynamic>;
|
||||
items[key] = data;
|
||||
});
|
||||
if (items.isEmpty) return null;
|
||||
return items;
|
||||
} catch (error) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _initStream(
|
||||
StreamController<Map<String, dynamic>> storage, String path) {
|
||||
var dataCol = html.window.localStorage.entries.singleWhere(
|
||||
(e) => e.key == path,
|
||||
orElse: () => const MapEntry('', ''),
|
||||
);
|
||||
try {
|
||||
if (dataCol.key != '') {
|
||||
final mapCol = json.decode(dataCol.value) as Map<String, dynamic>;
|
||||
mapCol.forEach((key, value) {
|
||||
final data = value as Map<String, dynamic>;
|
||||
storage.add(data);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
final _storageCache = <String, StreamController<Map<String, dynamic>>>{};
|
||||
|
||||
Future<dynamic> _readFromStorage(String path) async {
|
||||
final key = path.replaceAll(RegExp(r'[^\/]+\/?$'), '');
|
||||
final data = html.window.localStorage.entries.firstWhere(
|
||||
(i) => i.key == key,
|
||||
orElse: () => const MapEntry('', ''),
|
||||
);
|
||||
if (data != const MapEntry('', '')) {
|
||||
try {
|
||||
return json.decode(data.value) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> _writeToStorage(
|
||||
Map<String, dynamic> data,
|
||||
String path,
|
||||
) async {
|
||||
final key = path.replaceAll(RegExp(r'[^\/]+\/?$'), '');
|
||||
|
||||
final uri = Uri.parse(path);
|
||||
final id = uri.pathSegments.last;
|
||||
var dataCol = html.window.localStorage.entries.singleWhere(
|
||||
(e) => e.key == key,
|
||||
orElse: () => const MapEntry('', ''),
|
||||
);
|
||||
try {
|
||||
if (dataCol.key != '') {
|
||||
final mapCol = json.decode(dataCol.value) as Map<String, dynamic>;
|
||||
mapCol[id] = data;
|
||||
dataCol = MapEntry(id, json.encode(mapCol));
|
||||
html.window.localStorage.update(
|
||||
key,
|
||||
(value) => dataCol.value,
|
||||
ifAbsent: () => dataCol.value,
|
||||
);
|
||||
} else {
|
||||
html.window.localStorage.update(
|
||||
key,
|
||||
(value) => json.encode({id: data}),
|
||||
ifAbsent: () => json.encode({id: data}),
|
||||
);
|
||||
}
|
||||
// ignore: close_sinks
|
||||
final storage = _storageCache[key] ??
|
||||
_storageCache.putIfAbsent(
|
||||
key, () => StreamController<Map<String, dynamic>>.broadcast());
|
||||
|
||||
storage.sink.add(data);
|
||||
} catch (error) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> _deleteFromStorage(String path) async {
|
||||
if (path.endsWith('/')) {
|
||||
// If path is a directory path
|
||||
final dataCol = html.window.localStorage.entries.singleWhere(
|
||||
(element) => element.key == path,
|
||||
orElse: () => const MapEntry('', ''),
|
||||
);
|
||||
|
||||
try {
|
||||
if (dataCol.key != '') {
|
||||
html.window.localStorage.remove(dataCol.key);
|
||||
}
|
||||
} catch (error) {
|
||||
rethrow;
|
||||
}
|
||||
} else {
|
||||
// If path is a file path
|
||||
final uri = Uri.parse(path);
|
||||
final key = path.replaceAll(RegExp(r'[^\/]+\/?$'), '');
|
||||
final id = uri.pathSegments.last;
|
||||
var dataCol = html.window.localStorage.entries.singleWhere(
|
||||
(e) => e.key == key,
|
||||
orElse: () => const MapEntry('', ''),
|
||||
);
|
||||
|
||||
try {
|
||||
if (dataCol.key != '') {
|
||||
final mapCol = json.decode(dataCol.value) as Map<String, dynamic>;
|
||||
mapCol.remove(id);
|
||||
html.window.localStorage.update(
|
||||
key,
|
||||
(value) => json.encode(mapCol),
|
||||
ifAbsent: () => dataCol.value,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -197,7 +197,7 @@ sealed class TaskUpdate {
|
|||
/// [responseHeaders], [responseStatusCode], [mimeType] and [charSet].
|
||||
/// Note: header names in [responseHeaders] are converted to lowercase
|
||||
class TaskStatusUpdate extends TaskUpdate {
|
||||
final TaskStatus status;
|
||||
final TaskStatus status; // note: serialized as 'taskStatus'
|
||||
final TaskException? exception;
|
||||
final String? responseBody;
|
||||
final int? responseStatusCode;
|
||||
|
|
@ -220,8 +220,10 @@ class TaskStatusUpdate extends TaskUpdate {
|
|||
? TaskException.fromJson(json['exception'])
|
||||
: null,
|
||||
responseBody = json['responseBody'],
|
||||
responseHeaders = json['responseHeaders'],
|
||||
responseStatusCode = json['responseStatusCode'],
|
||||
responseHeaders = json['responseHeaders'] != null
|
||||
? Map.from(json['responseHeaders'])
|
||||
: null,
|
||||
responseStatusCode = (json['responseStatusCode'] as num?)?.toInt(),
|
||||
mimeType = json['mimeType'],
|
||||
charSet = json['charSet'],
|
||||
super.fromJson();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:mangayomi/services/background_downloader/src/base_downloader.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
|
@ -15,7 +16,6 @@ import 'package:path_provider/path_provider.dart';
|
|||
import 'file_downloader.dart';
|
||||
import 'models.dart';
|
||||
import 'utils.dart';
|
||||
import 'desktop/desktop_downloader.dart';
|
||||
|
||||
final _log = Logger('FileDownloader');
|
||||
|
||||
|
|
@ -67,12 +67,13 @@ base class Request {
|
|||
Request(
|
||||
{required String url,
|
||||
Map<String, String>? urlQueryParameters,
|
||||
this.headers = const {},
|
||||
Map<String, String>? headers,
|
||||
String? httpRequestMethod,
|
||||
post,
|
||||
this.retries = 0,
|
||||
DateTime? creationTime})
|
||||
: url = urlWithQueryParameters(url, urlQueryParameters),
|
||||
headers = headers ?? {},
|
||||
httpRequestMethod =
|
||||
httpRequestMethod?.toUpperCase() ?? (post == null ? 'GET' : 'POST'),
|
||||
post = post is Uint8List ? String.fromCharCodes(post) : post,
|
||||
|
|
@ -319,8 +320,9 @@ sealed class Task extends Request implements Comparable {
|
|||
'UploadTask' => UploadTask.fromJson(json),
|
||||
'MultiUploadTask' => MultiUploadTask.fromJson(json),
|
||||
'ParallelDownloadTask' => ParallelDownloadTask.fromJson(json),
|
||||
'DataTask' => DataTask.fromJson(json),
|
||||
_ => throw ArgumentError(
|
||||
'taskType not in [DownloadTask, UploadTask, MultiUploadTask, ParallelDownloadTask]')
|
||||
'taskType not in [DownloadTask, UploadTask, MultiUploadTask, ParallelDownloadTask, DataTask]')
|
||||
};
|
||||
|
||||
/// Create a new [Task] subclass from provided [jsonString]
|
||||
|
|
@ -1227,3 +1229,122 @@ final class ParallelDownloadTask extends DownloadTask {
|
|||
creationTime: creationTime ?? this.creationTime)
|
||||
..retriesRemaining = retriesRemaining ?? this.retriesRemaining;
|
||||
}
|
||||
|
||||
/// Class for background requests that do not involve a file
|
||||
///
|
||||
/// Closely resembles a Task, with fewer fields available during construction
|
||||
final class DataTask extends Task {
|
||||
/// Creates a [DataTask] that runs in the background, but does not involve a
|
||||
/// file
|
||||
///
|
||||
/// [taskId] must be unique. A unique id will be generated if omitted
|
||||
/// [url] properly encoded if necessary, can include query parameters
|
||||
/// [urlQueryParameters] may be added and will be appended to the [url], must
|
||||
/// be properly encoded if necessary
|
||||
/// [headers] an optional map of HTTP request headers
|
||||
/// [httpRequestMethod] the HTTP request method used (e.g. GET, POST)
|
||||
/// [post] String post body, encoded in utf8
|
||||
/// [json] if given will encode [json] to string and use as the [post] data
|
||||
/// [contentType] sets the Content-Type header to this value. If omitted and
|
||||
/// [post] is given, it will be set to 'text-plain; charset=utf-8' and if
|
||||
/// [json] is given, it will be set to 'application/json]
|
||||
/// [group] if set allows different callbacks or processing for different
|
||||
/// groups
|
||||
/// [updates] the kind of progress updates requested (only .status or none)
|
||||
/// [requiresWiFi] if set, will not start download until WiFi is available.
|
||||
/// If not set may start download over cellular network
|
||||
/// [retries] if >0 will retry a failed download this many times
|
||||
/// [priority] in range 0 <= priority <= 10 with 0 highest, defaults to 5
|
||||
/// [metaData] user data
|
||||
/// [displayName] human readable name for this task
|
||||
/// [creationTime] time of task creation, 'now' by default.
|
||||
DataTask(
|
||||
{String? taskId,
|
||||
required super.url,
|
||||
super.urlQueryParameters,
|
||||
super.headers,
|
||||
super.httpRequestMethod,
|
||||
String? post,
|
||||
Map<String, dynamic>? json,
|
||||
String? contentType,
|
||||
super.group,
|
||||
super.updates,
|
||||
super.requiresWiFi,
|
||||
super.retries,
|
||||
super.metaData,
|
||||
super.displayName,
|
||||
super.priority,
|
||||
super.creationTime})
|
||||
: assert(const [Updates.status, Updates.none].contains(updates),
|
||||
'DataTasks can only provide status updates'),
|
||||
super(
|
||||
post: json != null ? jsonEncode(json) : post,
|
||||
baseDirectory: BaseDirectory.temporary,
|
||||
allowPause: false) {
|
||||
// if no content-type header set, it is set to [contentType] or
|
||||
// (if post or json is given) to text/plain or application/json
|
||||
if (!headers.containsKey('Content-Type') &&
|
||||
!headers.containsKey('content-type')) {
|
||||
try {
|
||||
if (contentType != null) {
|
||||
headers['Content-Type'] = contentType;
|
||||
} else if ((post != null || json != null)) {
|
||||
assert((post != null) ^ (json != null),
|
||||
'Only post or json can be set, not both');
|
||||
headers['Content-Type'] =
|
||||
json != null ? 'application/json' : 'text/plain; charset=utf-8';
|
||||
}
|
||||
} on UnsupportedError {
|
||||
_log.warning(
|
||||
'Could not add Content-Type header as supplied header is const');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Task copyWith(
|
||||
{String? taskId,
|
||||
String? url,
|
||||
String? filename,
|
||||
Map<String, String>? headers,
|
||||
String? httpRequestMethod,
|
||||
Object? post,
|
||||
String? directory,
|
||||
BaseDirectory? baseDirectory,
|
||||
String? group,
|
||||
Updates? updates,
|
||||
bool? requiresWiFi,
|
||||
int? retries,
|
||||
int? retriesRemaining,
|
||||
bool? allowPause,
|
||||
int? priority,
|
||||
String? metaData,
|
||||
String? displayName,
|
||||
DateTime? creationTime}) =>
|
||||
DataTask(
|
||||
taskId: taskId ?? this.taskId,
|
||||
url: url ?? this.url,
|
||||
headers: headers ?? this.headers,
|
||||
httpRequestMethod: httpRequestMethod ?? this.httpRequestMethod,
|
||||
post: post as String? ?? this.post,
|
||||
group: group ?? this.group,
|
||||
updates: updates ?? this.updates,
|
||||
requiresWiFi: requiresWiFi ?? this.requiresWiFi,
|
||||
retries: retries ?? this.retries,
|
||||
priority: priority ?? this.priority,
|
||||
metaData: metaData ?? this.metaData,
|
||||
displayName: displayName ?? this.displayName,
|
||||
creationTime: creationTime ?? this.creationTime)
|
||||
..retriesRemaining = retriesRemaining ?? this.retriesRemaining;
|
||||
|
||||
/// Creates [DataTask] object from [json]
|
||||
DataTask.fromJson(super.json)
|
||||
: assert(
|
||||
json['taskType'] == 'DataTask',
|
||||
'The provided JSON map is not a DataTask, '
|
||||
'because key "taskType" is not "DataTask".'),
|
||||
super.fromJson();
|
||||
|
||||
@override
|
||||
String get taskType => 'DataTask';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue