mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-13 20:03:46 +00:00
feat(tasks): group extension folder downloads (#1594)
This commit is contained in:
1 parent
fc0a1e0ef9
commit
1e36599d52
84 files changed
+6690
-881
No files matched your search
@@ -0,0 +1,166 @@
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../../../api/model/create_task_batch.dart';
|
||||
import '../../../api/model/options.dart';
|
||||
import '../../../api/model/request.dart';
|
||||
import '../../../api/model/resource.dart';
|
||||
import '../../../api/model/resolve_result.dart';
|
||||
import '../../../api/model/resolve_task.dart';
|
||||
import '../../../core/capabilities/gopeed_capability.dart';
|
||||
|
||||
/// IDs identify sources within one preview and are never sent as group IDs.
|
||||
class PendingDownload {
|
||||
PendingDownload({required this.id, required this.request, required this.options});
|
||||
|
||||
final String id;
|
||||
final Request request;
|
||||
final Options options;
|
||||
ResolveResult? result;
|
||||
String? resolveError;
|
||||
String? createError;
|
||||
final Set<int> selected = {};
|
||||
bool created = false;
|
||||
bool resolving = false;
|
||||
|
||||
bool get loading => result == null && resolveError == null;
|
||||
bool get isGroup => result != null && result!.id.isEmpty && result!.res.name.trim().isNotEmpty && files.length > 1;
|
||||
bool get isFtp => RegExp(r'^(ftps?|ftpes):', caseSensitive: false).hasMatch(request.url.trim());
|
||||
bool get isBt {
|
||||
if (result?.id.isEmpty == true) return false;
|
||||
final url = request.url.trim().toLowerCase();
|
||||
if (url.startsWith('http:') || url.startsWith('https:') || isFtp) return false;
|
||||
return url.startsWith('magnet:') ||
|
||||
url.startsWith('data:application/x-bittorrent;base64,') ||
|
||||
url.split(RegExp(r'[?#]')).first.endsWith('.torrent');
|
||||
}
|
||||
|
||||
String get name {
|
||||
if (!isBt &&
|
||||
(!isFtp || result?.res.name.trim().isEmpty == true) &&
|
||||
result?.id.isNotEmpty == true &&
|
||||
files.length == 1 &&
|
||||
files.single.name.isNotEmpty) {
|
||||
return files.single.name;
|
||||
}
|
||||
if (result?.res.name.trim().isNotEmpty == true) return result!.res.name;
|
||||
return request.url;
|
||||
}
|
||||
|
||||
List<FileInfo> get files => result?.res.files ?? const [];
|
||||
bool get hasSelection => !created && result != null && selected.isNotEmpty;
|
||||
|
||||
Future<void> resolve(GopeedService service) async {
|
||||
if (result != null || resolving) return;
|
||||
resolving = true;
|
||||
resolveError = null;
|
||||
try {
|
||||
result = await service.resolve(ResolveTask(req: request));
|
||||
selected.addAll(List.generate(files.isEmpty ? 1 : files.length, (index) => index));
|
||||
} catch (error) {
|
||||
resolveError = error.toString();
|
||||
} finally {
|
||||
resolving = false;
|
||||
}
|
||||
}
|
||||
|
||||
List<CreateTaskBatchItem> creationItems() {
|
||||
final indexes = selected.toList()..sort();
|
||||
if (result!.id.isNotEmpty || files.isEmpty) {
|
||||
final opts = Options.fromJson(options.toJson());
|
||||
if (files.isNotEmpty) opts.selectFiles = indexes;
|
||||
if (indexes.length > 1) _omitChecksum(opts);
|
||||
return [
|
||||
CreateTaskBatchItem(
|
||||
rid: result!.id.isEmpty ? null : result!.id,
|
||||
req: result!.id.isEmpty ? request : null,
|
||||
opts: opts,
|
||||
),
|
||||
];
|
||||
}
|
||||
final group = isGroup;
|
||||
final resourceName = result!.res.name;
|
||||
final parentSegments = [
|
||||
if (!group && options.path.isNotEmpty) options.path,
|
||||
if (!group && resourceName.trim().isNotEmpty) resourceName,
|
||||
];
|
||||
final skipAutoCategorize = options.skipAutoCategorize || resourceName.isNotEmpty || files.length > 1;
|
||||
final httpUrl = RegExp(r'^https?:', caseSensitive: false);
|
||||
return indexes.map((index) {
|
||||
final file = files[index];
|
||||
final member = Options.fromJson(options.toJson());
|
||||
member.name = files.length == 1 && options.name.isNotEmpty ? options.name : file.name;
|
||||
final segments = [...parentSegments, if (file.path.isNotEmpty) file.path];
|
||||
member.path = segments.isEmpty ? '' : path.joinAll(segments);
|
||||
// The selected parent directory is persisted separately from members.
|
||||
member.asDefaultPath = false;
|
||||
member.skipAutoCategorize = skipAutoCategorize;
|
||||
final req = file.req ?? request;
|
||||
member.size = httpUrl.hasMatch(req.url) ? file.size : 0;
|
||||
if (RegExp(r'^(ftps?|ftpes):', caseSensitive: false).hasMatch(req.url)) {
|
||||
final extra = OptsExtraFtp.fromJson(Map<String, dynamic>.from(member.extra as Map? ?? const {}));
|
||||
extra.connections = extra.connections.clamp(0, 32);
|
||||
member.extra = extra.toJson();
|
||||
}
|
||||
if (indexes.length > 1) _omitChecksum(member);
|
||||
return CreateTaskBatchItem(req: req, opts: member);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void _omitChecksum(Options opts) {
|
||||
if (opts.extra is Map) opts.extra = Map<String, dynamic>.from(opts.extra! as Map)..remove('checksum');
|
||||
}
|
||||
}
|
||||
|
||||
/// A successful response completes the entire batch, including partial success.
|
||||
class DownloadSubmission {
|
||||
DownloadSubmission({required this.sources, required this.request});
|
||||
|
||||
final List<PendingDownload> sources;
|
||||
final CreateTaskBatch request;
|
||||
|
||||
Future<void> submit(GopeedService service) async {
|
||||
for (final source in sources) {
|
||||
source.createError = null;
|
||||
}
|
||||
try {
|
||||
await service.createTaskBatch(request);
|
||||
for (final source in sources) {
|
||||
source.created = true;
|
||||
}
|
||||
} catch (error) {
|
||||
for (final source in sources) {
|
||||
source.createError = error.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<DownloadSubmission> planDownloads(List<PendingDownload> sources) {
|
||||
final groups = <DownloadSubmission>[];
|
||||
final independent = <PendingDownload>[];
|
||||
for (final source in sources.where((source) => source.hasSelection)) {
|
||||
if (source.isGroup) {
|
||||
groups.add(
|
||||
DownloadSubmission(
|
||||
sources: [source],
|
||||
request: CreateTaskBatch(
|
||||
group: true,
|
||||
groupName: source.options.name.isNotEmpty ? source.options.name : source.result!.res.name.trim(),
|
||||
opts: source.options,
|
||||
reqs: source.creationItems(),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
independent.add(source);
|
||||
}
|
||||
}
|
||||
return [
|
||||
...groups,
|
||||
if (independent.isNotEmpty)
|
||||
DownloadSubmission(
|
||||
sources: independent,
|
||||
request: CreateTaskBatch(reqs: [for (final source in independent) ...source.creationItems()]),
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -23,6 +23,10 @@ class TasksState {
|
||||
final int uploadSpeedBytesPerSecond;
|
||||
final Object? lastError;
|
||||
|
||||
Iterable<TaskRecord> get allTasks => tasks.expand((task) => [task, ...task.members]);
|
||||
|
||||
TaskRecord? findTask(String id) => allTasks.where((task) => task.id == id).firstOrNull;
|
||||
|
||||
TasksState copyWith({
|
||||
List<TaskRecord>? tasks,
|
||||
int? downloadSpeedBytesPerSecond,
|
||||
@@ -122,13 +126,14 @@ class TasksController extends AsyncNotifier<TasksState> {
|
||||
Future<TasksState> _fetch() async {
|
||||
final tasks = await ref.read(gopeedServiceProvider).getTasks(api_model.Status.values);
|
||||
final transferSpeeds = aggregateTaskTransferSpeeds(tasks);
|
||||
tasks.sort((a, b) {
|
||||
if (a.status == api_model.Status.running && b.status != api_model.Status.running) return -1;
|
||||
if (a.status != api_model.Status.running && b.status == api_model.Status.running) return 1;
|
||||
return b.updatedAt.compareTo(a.updatedAt);
|
||||
});
|
||||
return TasksState(
|
||||
tasks: tasks.map(TaskRecord.fromApi).toList(growable: false),
|
||||
tasks: taskRecordsFromApi(tasks)
|
||||
..sort((a, b) {
|
||||
final aRunning = a.status == TaskStatus.downloading && !a.waiting;
|
||||
final bRunning = b.status == TaskStatus.downloading && !b.waiting;
|
||||
if (aRunning != bRunning) return aRunning ? -1 : 1;
|
||||
return (b.updatedAt ?? b.createdAt!).compareTo(a.updatedAt ?? a.createdAt!);
|
||||
}),
|
||||
downloadSpeedBytesPerSecond: transferSpeeds.downloadBytesPerSecond,
|
||||
uploadSpeedBytesPerSecond: transferSpeeds.uploadBytesPerSecond,
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user