import 'dart:convert'; import 'package:flutter/services.dart'; import '../models/torrent_info.dart'; /// Data classes for torrent metadata (matching Kotlin side) /// Базовая информация из magnet-ссылки class MagnetBasicInfo { final String name; final String infoHash; final List trackers; final int totalSize; MagnetBasicInfo({ required this.name, required this.infoHash, required this.trackers, this.totalSize = 0, }); factory MagnetBasicInfo.fromJson(Map json) { return MagnetBasicInfo( name: json['name'] as String, infoHash: json['infoHash'] as String, trackers: List.from(json['trackers'] as List), totalSize: json['totalSize'] as int? ?? 0, ); } Map 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 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 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 files; final List 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 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)) .toList(), subdirectories: (json['subdirectories'] as List) .map((dir) => DirectoryNode.fromJson(dir as Map)) .toList(), totalSize: json['totalSize'] as int, fileCount: json['fileCount'] as int, ); } } /// Структура файлов торрента class FileStructure { final DirectoryNode rootDirectory; final int totalFiles; final Map filesByType; FileStructure({ required this.rootDirectory, required this.totalFiles, required this.filesByType, }); factory FileStructure.fromJson(Map json) { return FileStructure( rootDirectory: DirectoryNode.fromJson(json['rootDirectory'] as Map), totalFiles: json['totalFiles'] as int, filesByType: Map.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 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 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), trackers: List.from(json['trackers'] as List), creationDate: json['creationDate'] as int, comment: json['comment'] as String, createdBy: json['createdBy'] as String, ); } /// Получить плоский список всех файлов List getAllFiles() { final List allFiles = []; _collectFiles(fileStructure.rootDirectory, allFiles); return allFiles; } void _collectFiles(DirectoryNode directory, List result) { result.addAll(directory.files); for (final subdir in directory.subdirectories) { _collectFiles(subdir, result); } } } 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 json) { return TorrentFileInfo( path: json['path'] as String, size: json['size'] as int, selected: json['selected'] as bool? ?? false, ); } Map 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 files; final String infoHash; TorrentMetadata({ required this.name, required this.totalSize, required this.files, required this.infoHash, }); factory TorrentMetadata.fromJson(Map 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)) .toList(), infoHash: json['infoHash'] as String, ); } Map 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 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 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> getAllDownloads() async { try { final String result = await _channel.invokeMethod('getTorrents'); final List jsonList = jsonDecode(result); return jsonList.map((json) { final data = json as Map; 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'); } } /// Get single torrent info static Future getTorrent(String infoHash) async { try { final String result = await _channel.invokeMethod('getTorrent', { 'infoHash': infoHash, }); final Map json = jsonDecode(result); return TorrentInfo.fromAndroidJson(json); } on PlatformException catch (e) { if (e.code == 'NOT_FOUND') return null; throw Exception('Failed to get torrent: ${e.message}'); } catch (e) { throw Exception('Failed to parse torrent: $e'); } } /// Get download progress for a torrent static Future getDownloadProgress(String infoHash) async { try { final torrentInfo = await getTorrent(infoHash); if (torrentInfo == null) return null; return DownloadProgress( infoHash: torrentInfo.infoHash, progress: torrentInfo.progress, downloadRate: torrentInfo.downloadSpeed, uploadRate: torrentInfo.uploadSpeed, numSeeds: torrentInfo.numSeeds, numPeers: torrentInfo.numPeers, state: torrentInfo.state, ); } catch (e) { return null; } } /// Pause download static Future pauseDownload(String infoHash) async { try { final bool result = await _channel.invokeMethod('pauseTorrent', { 'infoHash': infoHash, }); return result; } on PlatformException catch (e) { throw Exception('Failed to pause download: ${e.message}'); } } /// Resume download static Future resumeDownload(String infoHash) async { try { final bool result = await _channel.invokeMethod('resumeTorrent', { 'infoHash': infoHash, }); return result; } on PlatformException catch (e) { throw Exception('Failed to resume download: ${e.message}'); } } /// Cancel and remove download static Future cancelDownload(String infoHash) async { try { final bool result = await _channel.invokeMethod('removeTorrent', { 'infoHash': infoHash, 'deleteFiles': true, }); return result; } on PlatformException catch (e) { throw Exception('Failed to cancel download: ${e.message}'); } } /// Set file priority static Future setFilePriority(String infoHash, int fileIndex, FilePriority priority) async { try { final bool result = await _channel.invokeMethod('setFilePriority', { 'infoHash': infoHash, 'fileIndex': fileIndex, 'priority': priority.value, }); return result; } on PlatformException catch (e) { throw Exception('Failed to set file priority: ${e.message}'); } } /// Start downloading selected files from torrent static Future startDownload({ required String magnetLink, required List 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; } catch (e) { throw Exception('Failed to start download: $e'); } } // Legacy methods for compatibility with existing code /// Get torrent metadata from magnet link (legacy method) static Future 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 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 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'); } } } }