Files
neomovies-mobile/lib/data/services/torrent_platform_service.dart

597 lines
17 KiB
Dart
Raw Normal View History

2025-07-19 20:50:26 +03:00
import 'dart:convert';
import 'package:flutter/services.dart';
import '../models/torrent_info.dart';
2025-07-19 20:50:26 +03:00
/// Data classes for torrent metadata (matching Kotlin side)
/// Базовая информация из 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 {
static const MethodChannel _channel = MethodChannel('com.neo.neomovies_mobile/torrent');
/// Add torrent from magnet URI and start downloading
static Future<String> addTorrent({
required String magnetUri,
String? savePath,
}) async {
try {
final String infoHash = await _channel.invokeMethod('addTorrent', {
'magnetUri': magnetUri,
'savePath': savePath ?? '/storage/emulated/0/Download/NeoMovies',
});
return infoHash;
} on PlatformException catch (e) {
throw Exception('Failed to add torrent: ${e.message}');
}
}
/// Get all torrents
static Future<List<DownloadProgress>> getAllDownloads() async {
try {
final String result = await _channel.invokeMethod('getTorrents');
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();
} on PlatformException catch (e) {
throw Exception('Failed to get all downloads: ${e.message}');
} catch (e) {
throw Exception('Failed to parse downloads: $e');
}
}
2025-07-19 20:50:26 +03:00
/// Get single torrent info
static Future<TorrentInfo?> getTorrent(String infoHash) async {
2025-07-19 20:50:26 +03:00
try {
final String result = await _channel.invokeMethod('getTorrent', {
'infoHash': infoHash,
2025-07-19 20:50:26 +03:00
});
final Map<String, dynamic> json = jsonDecode(result);
return TorrentInfo.fromAndroidJson(json);
2025-07-19 20:50:26 +03:00
} on PlatformException catch (e) {
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) {
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 {
final torrentInfo = await getTorrent(infoHash);
if (torrentInfo == null) return null;
2025-07-19 20:50:26 +03: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) {
return null;
2025-07-19 20:50:26 +03:00
}
}
/// Pause download
static Future<bool> pauseDownload(String infoHash) async {
try {
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 {
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 {
final bool result = await _channel.invokeMethod('removeTorrent', {
2025-07-19 20:50:26 +03:00
'infoHash': infoHash,
'deleteFiles': true,
2025-07-19 20:50:26 +03:00
});
return result;
} on PlatformException catch (e) {
throw Exception('Failed to cancel download: ${e.message}');
}
}
/// Set file priority
static Future<bool> setFilePriority(String infoHash, int fileIndex, FilePriority priority) async {
2025-07-19 20:50:26 +03:00
try {
final bool result = await _channel.invokeMethod('setFilePriority', {
'infoHash': infoHash,
'fileIndex': fileIndex,
'priority': priority.value,
});
2025-07-19 20:50:26 +03:00
return result;
2025-07-19 20:50:26 +03:00
} on PlatformException catch (e) {
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) {
throw Exception('Failed to start download: $e');
2025-07-19 20:50:26 +03: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');
}
}
}