2025-07-19 20:50:26 +03:00
|
|
|
|
import 'dart:convert';
|
|
|
|
|
|
import 'package:flutter/services.dart';
|
2025-10-03 06:40:56 +00:00
|
|
|
|
import '../models/torrent_info.dart';
|
2025-07-19 20:50:26 +03:00
|
|
|
|
|
|
|
|
|
|
/// Data classes for torrent metadata (matching Kotlin side)
|
2025-08-03 18:24:12 +03:00
|
|
|
|
|
|
|
|
|
|
/// Базовая информация из magnet-ссылки
|
|
|
|
|
|
class MagnetBasicInfo {
|
|
|
|
|
|
final String name;
|
|
|
|
|
|
final String infoHash;
|
|
|
|
|
|
final List<String> trackers;
|
|
|
|
|
|
final int totalSize;
|
|
|
|
|
|
|
|
|
|
|
|
MagnetBasicInfo({
|
|
|
|
|
|
required this.name,
|
|
|
|
|
|
required this.infoHash,
|
|
|
|
|
|
required this.trackers,
|
|
|
|
|
|
this.totalSize = 0,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
factory MagnetBasicInfo.fromJson(Map<String, dynamic> json) {
|
|
|
|
|
|
return MagnetBasicInfo(
|
|
|
|
|
|
name: json['name'] as String,
|
|
|
|
|
|
infoHash: json['infoHash'] as String,
|
|
|
|
|
|
trackers: List<String>.from(json['trackers'] as List),
|
|
|
|
|
|
totalSize: json['totalSize'] as int? ?? 0,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Map<String, dynamic> toJson() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
'name': name,
|
|
|
|
|
|
'infoHash': infoHash,
|
|
|
|
|
|
'trackers': trackers,
|
|
|
|
|
|
'totalSize': totalSize,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Информация о файле в торренте
|
|
|
|
|
|
class FileInfo {
|
|
|
|
|
|
final String name;
|
|
|
|
|
|
final String path;
|
|
|
|
|
|
final int size;
|
|
|
|
|
|
final int index;
|
|
|
|
|
|
final String extension;
|
|
|
|
|
|
final bool isVideo;
|
|
|
|
|
|
final bool isAudio;
|
|
|
|
|
|
final bool isImage;
|
|
|
|
|
|
final bool isDocument;
|
|
|
|
|
|
final bool selected;
|
|
|
|
|
|
|
|
|
|
|
|
FileInfo({
|
|
|
|
|
|
required this.name,
|
|
|
|
|
|
required this.path,
|
|
|
|
|
|
required this.size,
|
|
|
|
|
|
required this.index,
|
|
|
|
|
|
this.extension = '',
|
|
|
|
|
|
this.isVideo = false,
|
|
|
|
|
|
this.isAudio = false,
|
|
|
|
|
|
this.isImage = false,
|
|
|
|
|
|
this.isDocument = false,
|
|
|
|
|
|
this.selected = false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
factory FileInfo.fromJson(Map<String, dynamic> json) {
|
|
|
|
|
|
return FileInfo(
|
|
|
|
|
|
name: json['name'] as String,
|
|
|
|
|
|
path: json['path'] as String,
|
|
|
|
|
|
size: json['size'] as int,
|
|
|
|
|
|
index: json['index'] as int,
|
|
|
|
|
|
extension: json['extension'] as String? ?? '',
|
|
|
|
|
|
isVideo: json['isVideo'] as bool? ?? false,
|
|
|
|
|
|
isAudio: json['isAudio'] as bool? ?? false,
|
|
|
|
|
|
isImage: json['isImage'] as bool? ?? false,
|
|
|
|
|
|
isDocument: json['isDocument'] as bool? ?? false,
|
|
|
|
|
|
selected: json['selected'] as bool? ?? false,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Map<String, dynamic> toJson() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
'name': name,
|
|
|
|
|
|
'path': path,
|
|
|
|
|
|
'size': size,
|
|
|
|
|
|
'index': index,
|
|
|
|
|
|
'extension': extension,
|
|
|
|
|
|
'isVideo': isVideo,
|
|
|
|
|
|
'isAudio': isAudio,
|
|
|
|
|
|
'isImage': isImage,
|
|
|
|
|
|
'isDocument': isDocument,
|
|
|
|
|
|
'selected': selected,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
FileInfo copyWith({
|
|
|
|
|
|
String? name,
|
|
|
|
|
|
String? path,
|
|
|
|
|
|
int? size,
|
|
|
|
|
|
int? index,
|
|
|
|
|
|
String? extension,
|
|
|
|
|
|
bool? isVideo,
|
|
|
|
|
|
bool? isAudio,
|
|
|
|
|
|
bool? isImage,
|
|
|
|
|
|
bool? isDocument,
|
|
|
|
|
|
bool? selected,
|
|
|
|
|
|
}) {
|
|
|
|
|
|
return FileInfo(
|
|
|
|
|
|
name: name ?? this.name,
|
|
|
|
|
|
path: path ?? this.path,
|
|
|
|
|
|
size: size ?? this.size,
|
|
|
|
|
|
index: index ?? this.index,
|
|
|
|
|
|
extension: extension ?? this.extension,
|
|
|
|
|
|
isVideo: isVideo ?? this.isVideo,
|
|
|
|
|
|
isAudio: isAudio ?? this.isAudio,
|
|
|
|
|
|
isImage: isImage ?? this.isImage,
|
|
|
|
|
|
isDocument: isDocument ?? this.isDocument,
|
|
|
|
|
|
selected: selected ?? this.selected,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Узел директории
|
|
|
|
|
|
class DirectoryNode {
|
|
|
|
|
|
final String name;
|
|
|
|
|
|
final String path;
|
|
|
|
|
|
final List<FileInfo> files;
|
|
|
|
|
|
final List<DirectoryNode> subdirectories;
|
|
|
|
|
|
final int totalSize;
|
|
|
|
|
|
final int fileCount;
|
|
|
|
|
|
|
|
|
|
|
|
DirectoryNode({
|
|
|
|
|
|
required this.name,
|
|
|
|
|
|
required this.path,
|
|
|
|
|
|
required this.files,
|
|
|
|
|
|
required this.subdirectories,
|
|
|
|
|
|
required this.totalSize,
|
|
|
|
|
|
required this.fileCount,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
factory DirectoryNode.fromJson(Map<String, dynamic> json) {
|
|
|
|
|
|
return DirectoryNode(
|
|
|
|
|
|
name: json['name'] as String,
|
|
|
|
|
|
path: json['path'] as String,
|
|
|
|
|
|
files: (json['files'] as List)
|
|
|
|
|
|
.map((file) => FileInfo.fromJson(file as Map<String, dynamic>))
|
|
|
|
|
|
.toList(),
|
|
|
|
|
|
subdirectories: (json['subdirectories'] as List)
|
|
|
|
|
|
.map((dir) => DirectoryNode.fromJson(dir as Map<String, dynamic>))
|
|
|
|
|
|
.toList(),
|
|
|
|
|
|
totalSize: json['totalSize'] as int,
|
|
|
|
|
|
fileCount: json['fileCount'] as int,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Структура файлов торрента
|
|
|
|
|
|
class FileStructure {
|
|
|
|
|
|
final DirectoryNode rootDirectory;
|
|
|
|
|
|
final int totalFiles;
|
|
|
|
|
|
final Map<String, int> filesByType;
|
|
|
|
|
|
|
|
|
|
|
|
FileStructure({
|
|
|
|
|
|
required this.rootDirectory,
|
|
|
|
|
|
required this.totalFiles,
|
|
|
|
|
|
required this.filesByType,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
factory FileStructure.fromJson(Map<String, dynamic> json) {
|
|
|
|
|
|
return FileStructure(
|
|
|
|
|
|
rootDirectory: DirectoryNode.fromJson(json['rootDirectory'] as Map<String, dynamic>),
|
|
|
|
|
|
totalFiles: json['totalFiles'] as int,
|
|
|
|
|
|
filesByType: Map<String, int>.from(json['filesByType'] as Map),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Полные метаданные торрента
|
|
|
|
|
|
class TorrentMetadataFull {
|
|
|
|
|
|
final String name;
|
|
|
|
|
|
final String infoHash;
|
|
|
|
|
|
final int totalSize;
|
|
|
|
|
|
final int pieceLength;
|
|
|
|
|
|
final int numPieces;
|
|
|
|
|
|
final FileStructure fileStructure;
|
|
|
|
|
|
final List<String> trackers;
|
|
|
|
|
|
final int creationDate;
|
|
|
|
|
|
final String comment;
|
|
|
|
|
|
final String createdBy;
|
|
|
|
|
|
|
|
|
|
|
|
TorrentMetadataFull({
|
|
|
|
|
|
required this.name,
|
|
|
|
|
|
required this.infoHash,
|
|
|
|
|
|
required this.totalSize,
|
|
|
|
|
|
required this.pieceLength,
|
|
|
|
|
|
required this.numPieces,
|
|
|
|
|
|
required this.fileStructure,
|
|
|
|
|
|
required this.trackers,
|
|
|
|
|
|
required this.creationDate,
|
|
|
|
|
|
required this.comment,
|
|
|
|
|
|
required this.createdBy,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
factory TorrentMetadataFull.fromJson(Map<String, dynamic> json) {
|
|
|
|
|
|
return TorrentMetadataFull(
|
|
|
|
|
|
name: json['name'] as String,
|
|
|
|
|
|
infoHash: json['infoHash'] as String,
|
|
|
|
|
|
totalSize: json['totalSize'] as int,
|
|
|
|
|
|
pieceLength: json['pieceLength'] as int,
|
|
|
|
|
|
numPieces: json['numPieces'] as int,
|
|
|
|
|
|
fileStructure: FileStructure.fromJson(json['fileStructure'] as Map<String, dynamic>),
|
|
|
|
|
|
trackers: List<String>.from(json['trackers'] as List),
|
|
|
|
|
|
creationDate: json['creationDate'] as int,
|
|
|
|
|
|
comment: json['comment'] as String,
|
|
|
|
|
|
createdBy: json['createdBy'] as String,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Получить плоский список всех файлов
|
|
|
|
|
|
List<FileInfo> getAllFiles() {
|
|
|
|
|
|
final List<FileInfo> allFiles = [];
|
|
|
|
|
|
_collectFiles(fileStructure.rootDirectory, allFiles);
|
|
|
|
|
|
return allFiles;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _collectFiles(DirectoryNode directory, List<FileInfo> result) {
|
|
|
|
|
|
result.addAll(directory.files);
|
|
|
|
|
|
for (final subdir in directory.subdirectories) {
|
|
|
|
|
|
_collectFiles(subdir, result);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-19 20:50:26 +03:00
|
|
|
|
class TorrentFileInfo {
|
|
|
|
|
|
final String path;
|
|
|
|
|
|
final int size;
|
|
|
|
|
|
final bool selected;
|
|
|
|
|
|
|
|
|
|
|
|
TorrentFileInfo({
|
|
|
|
|
|
required this.path,
|
|
|
|
|
|
required this.size,
|
|
|
|
|
|
this.selected = false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
factory TorrentFileInfo.fromJson(Map<String, dynamic> json) {
|
|
|
|
|
|
return TorrentFileInfo(
|
|
|
|
|
|
path: json['path'] as String,
|
|
|
|
|
|
size: json['size'] as int,
|
|
|
|
|
|
selected: json['selected'] as bool? ?? false,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Map<String, dynamic> toJson() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
'path': path,
|
|
|
|
|
|
'size': size,
|
|
|
|
|
|
'selected': selected,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TorrentFileInfo copyWith({
|
|
|
|
|
|
String? path,
|
|
|
|
|
|
int? size,
|
|
|
|
|
|
bool? selected,
|
|
|
|
|
|
}) {
|
|
|
|
|
|
return TorrentFileInfo(
|
|
|
|
|
|
path: path ?? this.path,
|
|
|
|
|
|
size: size ?? this.size,
|
|
|
|
|
|
selected: selected ?? this.selected,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class TorrentMetadata {
|
|
|
|
|
|
final String name;
|
|
|
|
|
|
final int totalSize;
|
|
|
|
|
|
final List<TorrentFileInfo> files;
|
|
|
|
|
|
final String infoHash;
|
|
|
|
|
|
|
|
|
|
|
|
TorrentMetadata({
|
|
|
|
|
|
required this.name,
|
|
|
|
|
|
required this.totalSize,
|
|
|
|
|
|
required this.files,
|
|
|
|
|
|
required this.infoHash,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
factory TorrentMetadata.fromJson(Map<String, dynamic> json) {
|
|
|
|
|
|
return TorrentMetadata(
|
|
|
|
|
|
name: json['name'] as String,
|
|
|
|
|
|
totalSize: json['totalSize'] as int,
|
|
|
|
|
|
files: (json['files'] as List)
|
|
|
|
|
|
.map((file) => TorrentFileInfo.fromJson(file as Map<String, dynamic>))
|
|
|
|
|
|
.toList(),
|
|
|
|
|
|
infoHash: json['infoHash'] as String,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Map<String, dynamic> toJson() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
'name': name,
|
|
|
|
|
|
'totalSize': totalSize,
|
|
|
|
|
|
'files': files.map((file) => file.toJson()).toList(),
|
|
|
|
|
|
'infoHash': infoHash,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class DownloadProgress {
|
|
|
|
|
|
final String infoHash;
|
|
|
|
|
|
final double progress;
|
|
|
|
|
|
final int downloadRate;
|
|
|
|
|
|
final int uploadRate;
|
|
|
|
|
|
final int numSeeds;
|
|
|
|
|
|
final int numPeers;
|
|
|
|
|
|
final String state;
|
|
|
|
|
|
|
|
|
|
|
|
DownloadProgress({
|
|
|
|
|
|
required this.infoHash,
|
|
|
|
|
|
required this.progress,
|
|
|
|
|
|
required this.downloadRate,
|
|
|
|
|
|
required this.uploadRate,
|
|
|
|
|
|
required this.numSeeds,
|
|
|
|
|
|
required this.numPeers,
|
|
|
|
|
|
required this.state,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
factory DownloadProgress.fromJson(Map<String, dynamic> json) {
|
|
|
|
|
|
return DownloadProgress(
|
|
|
|
|
|
infoHash: json['infoHash'] as String,
|
|
|
|
|
|
progress: (json['progress'] as num).toDouble(),
|
|
|
|
|
|
downloadRate: json['downloadRate'] as int,
|
|
|
|
|
|
uploadRate: json['uploadRate'] as int,
|
|
|
|
|
|
numSeeds: json['numSeeds'] as int,
|
|
|
|
|
|
numPeers: json['numPeers'] as int,
|
|
|
|
|
|
state: json['state'] as String,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Platform service for torrent operations using jlibtorrent on Android
|
|
|
|
|
|
class TorrentPlatformService {
|
2025-08-03 18:24:12 +03:00
|
|
|
|
static const MethodChannel _channel = MethodChannel('com.neo.neomovies_mobile/torrent');
|
|
|
|
|
|
|
2025-10-03 06:40:56 +00:00
|
|
|
|
/// Add torrent from magnet URI and start downloading
|
|
|
|
|
|
static Future<String> addTorrent({
|
|
|
|
|
|
required String magnetUri,
|
|
|
|
|
|
String? savePath,
|
|
|
|
|
|
}) async {
|
2025-08-03 18:24:12 +03:00
|
|
|
|
try {
|
2025-10-03 06:40:56 +00:00
|
|
|
|
final String infoHash = await _channel.invokeMethod('addTorrent', {
|
2025-08-03 18:24:12 +03:00
|
|
|
|
'magnetUri': magnetUri,
|
2025-10-03 06:40:56 +00:00
|
|
|
|
'savePath': savePath ?? '/storage/emulated/0/Download/NeoMovies',
|
2025-08-03 18:24:12 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-03 06:40:56 +00:00
|
|
|
|
return infoHash;
|
2025-08-03 18:24:12 +03:00
|
|
|
|
} on PlatformException catch (e) {
|
2025-10-03 06:40:56 +00:00
|
|
|
|
throw Exception('Failed to add torrent: ${e.message}');
|
2025-08-03 18:24:12 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-03 06:40:56 +00:00
|
|
|
|
/// Get all torrents
|
|
|
|
|
|
static Future<List<DownloadProgress>> getAllDownloads() async {
|
2025-08-03 18:24:12 +03:00
|
|
|
|
try {
|
2025-10-03 06:40:56 +00:00
|
|
|
|
final String result = await _channel.invokeMethod('getTorrents');
|
2025-08-03 18:24:12 +03:00
|
|
|
|
|
2025-10-03 06:40:56 +00:00
|
|
|
|
final List<dynamic> jsonList = jsonDecode(result);
|
|
|
|
|
|
return jsonList.map((json) {
|
|
|
|
|
|
final data = json as Map<String, dynamic>;
|
|
|
|
|
|
return DownloadProgress(
|
|
|
|
|
|
infoHash: data['infoHash'] as String,
|
|
|
|
|
|
progress: (data['progress'] as num).toDouble(),
|
|
|
|
|
|
downloadRate: data['downloadSpeed'] as int,
|
|
|
|
|
|
uploadRate: data['uploadSpeed'] as int,
|
|
|
|
|
|
numSeeds: data['numSeeds'] as int,
|
|
|
|
|
|
numPeers: data['numPeers'] as int,
|
|
|
|
|
|
state: data['state'] as String,
|
|
|
|
|
|
);
|
|
|
|
|
|
}).toList();
|
2025-08-03 18:24:12 +03:00
|
|
|
|
} on PlatformException catch (e) {
|
2025-10-03 06:40:56 +00:00
|
|
|
|
throw Exception('Failed to get all downloads: ${e.message}');
|
2025-08-03 18:24:12 +03:00
|
|
|
|
} catch (e) {
|
2025-10-03 06:40:56 +00:00
|
|
|
|
throw Exception('Failed to parse downloads: $e');
|
2025-08-03 18:24:12 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-07-19 20:50:26 +03:00
|
|
|
|
|
2025-10-03 06:40:56 +00:00
|
|
|
|
/// Get single torrent info
|
|
|
|
|
|
static Future<TorrentInfo?> getTorrent(String infoHash) async {
|
2025-07-19 20:50:26 +03:00
|
|
|
|
try {
|
2025-10-03 06:40:56 +00:00
|
|
|
|
final String result = await _channel.invokeMethod('getTorrent', {
|
|
|
|
|
|
'infoHash': infoHash,
|
2025-07-19 20:50:26 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
final Map<String, dynamic> json = jsonDecode(result);
|
2025-10-03 06:40:56 +00:00
|
|
|
|
return TorrentInfo.fromAndroidJson(json);
|
2025-07-19 20:50:26 +03:00
|
|
|
|
} on PlatformException catch (e) {
|
2025-10-03 06:40:56 +00:00
|
|
|
|
if (e.code == 'NOT_FOUND') return null;
|
|
|
|
|
|
throw Exception('Failed to get torrent: ${e.message}');
|
2025-07-19 20:50:26 +03:00
|
|
|
|
} catch (e) {
|
2025-10-03 06:40:56 +00:00
|
|
|
|
throw Exception('Failed to parse torrent: $e');
|
2025-07-19 20:50:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Get download progress for a torrent
|
|
|
|
|
|
static Future<DownloadProgress?> getDownloadProgress(String infoHash) async {
|
|
|
|
|
|
try {
|
2025-10-03 06:40:56 +00:00
|
|
|
|
final torrentInfo = await getTorrent(infoHash);
|
|
|
|
|
|
if (torrentInfo == null) return null;
|
2025-07-19 20:50:26 +03:00
|
|
|
|
|
2025-10-03 06:40:56 +00:00
|
|
|
|
return DownloadProgress(
|
|
|
|
|
|
infoHash: torrentInfo.infoHash,
|
|
|
|
|
|
progress: torrentInfo.progress,
|
|
|
|
|
|
downloadRate: torrentInfo.downloadSpeed,
|
|
|
|
|
|
uploadRate: torrentInfo.uploadSpeed,
|
|
|
|
|
|
numSeeds: torrentInfo.numSeeds,
|
|
|
|
|
|
numPeers: torrentInfo.numPeers,
|
|
|
|
|
|
state: torrentInfo.state,
|
|
|
|
|
|
);
|
2025-07-19 20:50:26 +03:00
|
|
|
|
} catch (e) {
|
2025-10-03 06:40:56 +00:00
|
|
|
|
return null;
|
2025-07-19 20:50:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Pause download
|
|
|
|
|
|
static Future<bool> pauseDownload(String infoHash) async {
|
|
|
|
|
|
try {
|
2025-10-03 06:40:56 +00:00
|
|
|
|
final bool result = await _channel.invokeMethod('pauseTorrent', {
|
2025-07-19 20:50:26 +03:00
|
|
|
|
'infoHash': infoHash,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
} on PlatformException catch (e) {
|
|
|
|
|
|
throw Exception('Failed to pause download: ${e.message}');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Resume download
|
|
|
|
|
|
static Future<bool> resumeDownload(String infoHash) async {
|
|
|
|
|
|
try {
|
2025-10-03 06:40:56 +00:00
|
|
|
|
final bool result = await _channel.invokeMethod('resumeTorrent', {
|
2025-07-19 20:50:26 +03:00
|
|
|
|
'infoHash': infoHash,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
} on PlatformException catch (e) {
|
|
|
|
|
|
throw Exception('Failed to resume download: ${e.message}');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Cancel and remove download
|
|
|
|
|
|
static Future<bool> cancelDownload(String infoHash) async {
|
|
|
|
|
|
try {
|
2025-10-03 06:40:56 +00:00
|
|
|
|
final bool result = await _channel.invokeMethod('removeTorrent', {
|
2025-07-19 20:50:26 +03:00
|
|
|
|
'infoHash': infoHash,
|
2025-10-03 06:40:56 +00:00
|
|
|
|
'deleteFiles': true,
|
2025-07-19 20:50:26 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
} on PlatformException catch (e) {
|
|
|
|
|
|
throw Exception('Failed to cancel download: ${e.message}');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-03 06:40:56 +00:00
|
|
|
|
/// Set file priority
|
|
|
|
|
|
static Future<bool> setFilePriority(String infoHash, int fileIndex, FilePriority priority) async {
|
2025-07-19 20:50:26 +03:00
|
|
|
|
try {
|
2025-10-03 06:40:56 +00:00
|
|
|
|
final bool result = await _channel.invokeMethod('setFilePriority', {
|
|
|
|
|
|
'infoHash': infoHash,
|
|
|
|
|
|
'fileIndex': fileIndex,
|
|
|
|
|
|
'priority': priority.value,
|
|
|
|
|
|
});
|
2025-07-19 20:50:26 +03:00
|
|
|
|
|
2025-10-03 06:40:56 +00:00
|
|
|
|
return result;
|
2025-07-19 20:50:26 +03:00
|
|
|
|
} on PlatformException catch (e) {
|
2025-10-03 06:40:56 +00:00
|
|
|
|
throw Exception('Failed to set file priority: ${e.message}');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Start downloading selected files from torrent
|
|
|
|
|
|
static Future<String> startDownload({
|
|
|
|
|
|
required String magnetLink,
|
|
|
|
|
|
required List<int> selectedFiles,
|
|
|
|
|
|
String? downloadPath,
|
|
|
|
|
|
}) async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// First add the torrent
|
|
|
|
|
|
final String infoHash = await addTorrent(
|
|
|
|
|
|
magnetUri: magnetLink,
|
|
|
|
|
|
savePath: downloadPath,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Wait for metadata to be received
|
|
|
|
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
|
|
|
|
|
|
|
|
|
|
// Set file priorities
|
|
|
|
|
|
final torrentInfo = await getTorrent(infoHash);
|
|
|
|
|
|
if (torrentInfo != null) {
|
|
|
|
|
|
for (int i = 0; i < torrentInfo.files.length; i++) {
|
|
|
|
|
|
final priority = selectedFiles.contains(i)
|
|
|
|
|
|
? FilePriority.NORMAL
|
|
|
|
|
|
: FilePriority.DONT_DOWNLOAD;
|
|
|
|
|
|
await setFilePriority(infoHash, i, priority);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return infoHash;
|
2025-07-19 20:50:26 +03:00
|
|
|
|
} catch (e) {
|
2025-10-03 06:40:56 +00:00
|
|
|
|
throw Exception('Failed to start download: $e');
|
2025-07-19 20:50:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-03 06:40:56 +00:00
|
|
|
|
|
|
|
|
|
|
// Legacy methods for compatibility with existing code
|
|
|
|
|
|
|
|
|
|
|
|
/// Get torrent metadata from magnet link (legacy method)
|
|
|
|
|
|
static Future<TorrentMetadata> getTorrentMetadata(String magnetLink) async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// This is a simplified implementation that adds the torrent and gets metadata
|
|
|
|
|
|
final infoHash = await addTorrent(magnetUri: magnetLink);
|
|
|
|
|
|
await Future.delayed(const Duration(seconds: 3)); // Wait for metadata
|
|
|
|
|
|
|
|
|
|
|
|
final torrentInfo = await getTorrent(infoHash);
|
|
|
|
|
|
if (torrentInfo == null) {
|
|
|
|
|
|
throw Exception('Failed to get torrent metadata');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return TorrentMetadata(
|
|
|
|
|
|
name: torrentInfo.name,
|
|
|
|
|
|
totalSize: torrentInfo.totalSize,
|
|
|
|
|
|
files: torrentInfo.files.map((file) => TorrentFileInfo(
|
|
|
|
|
|
path: file.path,
|
|
|
|
|
|
size: file.size,
|
|
|
|
|
|
selected: file.priority > FilePriority.DONT_DOWNLOAD,
|
|
|
|
|
|
)).toList(),
|
|
|
|
|
|
infoHash: torrentInfo.infoHash,
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
throw Exception('Failed to get torrent metadata: $e');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Получить базовую информацию из magnet-ссылки (legacy)
|
|
|
|
|
|
static Future<MagnetBasicInfo> parseMagnetBasicInfo(String magnetUri) async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Parse magnet URI manually since Android implementation doesn't have this
|
|
|
|
|
|
final uri = Uri.parse(magnetUri);
|
|
|
|
|
|
final params = uri.queryParameters;
|
|
|
|
|
|
|
|
|
|
|
|
return MagnetBasicInfo(
|
|
|
|
|
|
name: params['dn'] ?? 'Unknown',
|
|
|
|
|
|
infoHash: params['xt']?.replaceFirst('urn:btih:', '') ?? '',
|
|
|
|
|
|
trackers: params['tr'] != null ? [params['tr']!] : [],
|
|
|
|
|
|
totalSize: 0,
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
throw Exception('Failed to parse magnet basic info: $e');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Получить полные метаданные торрента (legacy)
|
|
|
|
|
|
static Future<TorrentMetadataFull> fetchFullMetadata(String magnetUri) async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
final basicInfo = await parseMagnetBasicInfo(magnetUri);
|
|
|
|
|
|
final metadata = await getTorrentMetadata(magnetUri);
|
|
|
|
|
|
|
|
|
|
|
|
return TorrentMetadataFull(
|
|
|
|
|
|
name: metadata.name,
|
|
|
|
|
|
infoHash: metadata.infoHash,
|
|
|
|
|
|
totalSize: metadata.totalSize,
|
|
|
|
|
|
pieceLength: 0,
|
|
|
|
|
|
numPieces: 0,
|
|
|
|
|
|
fileStructure: FileStructure(
|
|
|
|
|
|
rootDirectory: DirectoryNode(
|
|
|
|
|
|
name: metadata.name,
|
|
|
|
|
|
path: '/',
|
|
|
|
|
|
files: metadata.files.map((file) => FileInfo(
|
|
|
|
|
|
name: file.path.split('/').last,
|
|
|
|
|
|
path: file.path,
|
|
|
|
|
|
size: file.size,
|
|
|
|
|
|
index: metadata.files.indexOf(file),
|
|
|
|
|
|
)).toList(),
|
|
|
|
|
|
subdirectories: [],
|
|
|
|
|
|
totalSize: metadata.totalSize,
|
|
|
|
|
|
fileCount: metadata.files.length,
|
|
|
|
|
|
),
|
|
|
|
|
|
totalFiles: metadata.files.length,
|
|
|
|
|
|
filesByType: {'video': metadata.files.length},
|
|
|
|
|
|
),
|
|
|
|
|
|
trackers: basicInfo.trackers,
|
|
|
|
|
|
creationDate: 0,
|
|
|
|
|
|
comment: '',
|
|
|
|
|
|
createdBy: '',
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
throw Exception('Failed to fetch full metadata: $e');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-07-19 20:50:26 +03:00
|
|
|
|
}
|