mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-08 01:20:27 +00:00
167 lines
5.9 KiB
Dart
167 lines
5.9 KiB
Dart
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()]),
|
|
),
|
|
];
|
|
}
|