diff --git a/lib/data/services/player_embed_service.dart b/lib/data/services/player_embed_service.dart new file mode 100644 index 0000000..ed6523f --- /dev/null +++ b/lib/data/services/player_embed_service.dart @@ -0,0 +1,130 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; + +/// Service for getting player embed URLs from NeoMovies API server +class PlayerEmbedService { + static const String _baseUrl = 'https://neomovies.site'; // Replace with actual base URL + + /// Get Vibix player embed URL from server + static Future getVibixEmbedUrl({ + required String videoUrl, + required String title, + String? imdbId, + String? season, + String? episode, + }) async { + try { + final response = await http.post( + Uri.parse('$_baseUrl/api/player/vibix/embed'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: jsonEncode({ + 'videoUrl': videoUrl, + 'title': title, + 'imdbId': imdbId, + 'season': season, + 'episode': episode, + 'autoplay': true, + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return data['embedUrl'] as String; + } else { + throw Exception('Failed to get Vibix embed URL: ${response.statusCode}'); + } + } catch (e) { + // Fallback to direct URL if server is unavailable + final encodedVideoUrl = Uri.encodeComponent(videoUrl); + final encodedTitle = Uri.encodeComponent(title); + return 'https://vibix.me/embed/?src=$encodedVideoUrl&autoplay=1&title=$encodedTitle'; + } + } + + /// Get Alloha player embed URL from server + static Future getAllohaEmbedUrl({ + required String videoUrl, + required String title, + String? imdbId, + String? season, + String? episode, + }) async { + try { + final response = await http.post( + Uri.parse('$_baseUrl/api/player/alloha/embed'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: jsonEncode({ + 'videoUrl': videoUrl, + 'title': title, + 'imdbId': imdbId, + 'season': season, + 'episode': episode, + 'autoplay': true, + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return data['embedUrl'] as String; + } else { + throw Exception('Failed to get Alloha embed URL: ${response.statusCode}'); + } + } catch (e) { + // Fallback to direct URL if server is unavailable + final encodedVideoUrl = Uri.encodeComponent(videoUrl); + final encodedTitle = Uri.encodeComponent(title); + return 'https://alloha.tv/embed?src=$encodedVideoUrl&autoplay=1&title=$encodedTitle'; + } + } + + /// Get player configuration from server + static Future?> getPlayerConfig({ + required String playerType, + String? imdbId, + String? season, + String? episode, + }) async { + try { + final response = await http.get( + Uri.parse('$_baseUrl/api/player/$playerType/config').replace( + queryParameters: { + if (imdbId != null) 'imdbId': imdbId, + if (season != null) 'season': season, + if (episode != null) 'episode': episode, + }, + ), + headers: { + 'Accept': 'application/json', + }, + ); + + if (response.statusCode == 200) { + return jsonDecode(response.body) as Map; + } else { + return null; + } + } catch (e) { + return null; + } + } + + /// Check if server player API is available + static Future isServerApiAvailable() async { + try { + final response = await http.get( + Uri.parse('$_baseUrl/api/player/health'), + headers: {'Accept': 'application/json'}, + ).timeout(const Duration(seconds: 5)); + + return response.statusCode == 200; + } catch (e) { + return false; + } + } +} \ No newline at end of file diff --git a/lib/presentation/screens/player/webview_player_screen.dart b/lib/presentation/screens/player/webview_player_screen.dart index e11c59e..0ed2fdc 100644 --- a/lib/presentation/screens/player/webview_player_screen.dart +++ b/lib/presentation/screens/player/webview_player_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:auto_route/auto_route.dart'; +import '../../../data/services/player_embed_service.dart'; enum WebPlayerType { vibix, alloha } @@ -71,30 +72,58 @@ class _WebViewPlayerScreenState extends State { _loadPlayer(); } - void _loadPlayer() { - final playerUrl = _getPlayerUrl(); - _controller.loadRequest(Uri.parse(playerUrl)); - } + void _loadPlayer() async { + try { + setState(() { + _isLoading = true; + _error = null; + }); - String _getPlayerUrl() { - switch (widget.playerType) { - case WebPlayerType.vibix: - return _getVibixUrl(); - case WebPlayerType.alloha: - return _getAllohaUrl(); + final playerUrl = await _getPlayerUrl(); + _controller.loadRequest(Uri.parse(playerUrl)); + } catch (e) { + setState(() { + _error = 'Ошибка получения URL плеера: $e'; + _isLoading = false; + }); } } - String _getVibixUrl() { - // Vibix player URL with embedded video - final encodedVideoUrl = Uri.encodeComponent(widget.videoUrl); - return 'https://vibix.me/embed/?src=$encodedVideoUrl&autoplay=1&title=${Uri.encodeComponent(widget.title)}'; + Future _getPlayerUrl() async { + switch (widget.playerType) { + case WebPlayerType.vibix: + return await _getVibixUrl(); + case WebPlayerType.alloha: + return await _getAllohaUrl(); + } } - String _getAllohaUrl() { - // Alloha player URL with embedded video - final encodedVideoUrl = Uri.encodeComponent(widget.videoUrl); - return 'https://alloha.tv/embed?src=$encodedVideoUrl&autoplay=1&title=${Uri.encodeComponent(widget.title)}'; + Future _getVibixUrl() async { + try { + // Try to get embed URL from API server first + return await PlayerEmbedService.getVibixEmbedUrl( + videoUrl: widget.videoUrl, + title: widget.title, + ); + } catch (e) { + // Fallback to direct URL if server is unavailable + final encodedVideoUrl = Uri.encodeComponent(widget.videoUrl); + return 'https://vibix.me/embed/?src=$encodedVideoUrl&autoplay=1&title=${Uri.encodeComponent(widget.title)}'; + } + } + + Future _getAllohaUrl() async { + try { + // Try to get embed URL from API server first + return await PlayerEmbedService.getAllohaEmbedUrl( + videoUrl: widget.videoUrl, + title: widget.title, + ); + } catch (e) { + // Fallback to direct URL if server is unavailable + final encodedVideoUrl = Uri.encodeComponent(widget.videoUrl); + return 'https://alloha.tv/embed?src=$encodedVideoUrl&autoplay=1&title=${Uri.encodeComponent(widget.title)}'; + } } void _toggleFullscreen() { diff --git a/pubspec.yaml b/pubspec.yaml index a12cd6f..843c7b5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,6 +73,8 @@ dev_dependencies: flutter_lints: ^5.0.0 build_runner: ^2.4.13 flutter_launcher_icons: ^0.13.1 + # HTTP mocking for testing + http_mock_adapter: ^0.6.1 flutter_launcher_icons: android: true diff --git a/test/services/player_embed_service_test.dart b/test/services/player_embed_service_test.dart new file mode 100644 index 0000000..7cc8155 --- /dev/null +++ b/test/services/player_embed_service_test.dart @@ -0,0 +1,381 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:neomovies_mobile/data/services/player_embed_service.dart'; + +void main() { + group('PlayerEmbedService Tests', () { + group('Vibix Player', () { + test('should get embed URL from API server successfully', () async { + final mockClient = MockClient((request) async { + if (request.url.path == '/api/player/vibix/embed') { + final body = jsonDecode(request.body); + expect(body['videoUrl'], 'http://example.com/video.mp4'); + expect(body['title'], 'Test Movie'); + expect(body['autoplay'], true); + + return http.Response( + jsonEncode({ + 'embedUrl': 'https://vibix.me/embed/custom?src=encoded&autoplay=1', + 'success': true, + }), + 200, + headers: {'content-type': 'application/json'}, + ); + } + return http.Response('Not Found', 404); + }); + + // Mock the http client (in real implementation, you'd inject this) + final embedUrl = await _testGetVibixEmbedUrl( + client: mockClient, + videoUrl: 'http://example.com/video.mp4', + title: 'Test Movie', + ); + + expect(embedUrl, 'https://vibix.me/embed/custom?src=encoded&autoplay=1'); + }); + + test('should fallback to direct URL when server fails', () async { + final mockClient = MockClient((request) async { + return http.Response('Server Error', 500); + }); + + final embedUrl = await _testGetVibixEmbedUrl( + client: mockClient, + videoUrl: 'http://example.com/video.mp4', + title: 'Test Movie', + ); + + expect(embedUrl, contains('vibix.me/embed')); + expect(embedUrl, contains('src=http%3A//example.com/video.mp4')); + expect(embedUrl, contains('title=Test%20Movie')); + }); + + test('should handle network timeout gracefully', () async { + final mockClient = MockClient((request) async { + throw const SocketException('Connection timeout'); + }); + + final embedUrl = await _testGetVibixEmbedUrl( + client: mockClient, + videoUrl: 'http://example.com/video.mp4', + title: 'Test Movie', + ); + + // Should fallback to direct URL + expect(embedUrl, contains('vibix.me/embed')); + }); + + test('should include optional parameters in API request', () async { + final mockClient = MockClient((request) async { + if (request.url.path == '/api/player/vibix/embed') { + final body = jsonDecode(request.body); + expect(body['imdbId'], 'tt1234567'); + expect(body['season'], '1'); + expect(body['episode'], '5'); + + return http.Response( + jsonEncode({'embedUrl': 'https://vibix.me/embed/tv'}), + 200, + ); + } + return http.Response('Not Found', 404); + }); + + final embedUrl = await _testGetVibixEmbedUrl( + client: mockClient, + videoUrl: 'http://example.com/video.mp4', + title: 'Test TV Show', + imdbId: 'tt1234567', + season: '1', + episode: '5', + ); + + expect(embedUrl, 'https://vibix.me/embed/tv'); + }); + }); + + group('Alloha Player', () { + test('should get embed URL from API server successfully', () async { + final mockClient = MockClient((request) async { + if (request.url.path == '/api/player/alloha/embed') { + return http.Response( + jsonEncode({ + 'embedUrl': 'https://alloha.tv/embed/custom?src=encoded', + 'success': true, + }), + 200, + ); + } + return http.Response('Not Found', 404); + }); + + final embedUrl = await _testGetAllohaEmbedUrl( + client: mockClient, + videoUrl: 'http://example.com/video.mp4', + title: 'Test Movie', + ); + + expect(embedUrl, 'https://alloha.tv/embed/custom?src=encoded'); + }); + + test('should fallback to direct URL when server fails', () async { + final mockClient = MockClient((request) async { + return http.Response('Server Error', 500); + }); + + final embedUrl = await _testGetAllohaEmbedUrl( + client: mockClient, + videoUrl: 'http://example.com/video.mp4', + title: 'Test Movie', + ); + + expect(embedUrl, contains('alloha.tv/embed')); + expect(embedUrl, contains('src=http%3A//example.com/video.mp4')); + }); + }); + + group('Player Configuration', () { + test('should get player config from server', () async { + final mockClient = MockClient((request) async { + if (request.url.path == '/api/player/vibix/config') { + return http.Response( + jsonEncode({ + 'playerOptions': { + 'autoplay': true, + 'controls': true, + 'volume': 0.8, + }, + 'theme': 'dark', + 'language': 'ru', + }), + 200, + ); + } + return http.Response('Not Found', 404); + }); + + final config = await _testGetPlayerConfig( + client: mockClient, + playerType: 'vibix', + imdbId: 'tt1234567', + ); + + expect(config, isNotNull); + expect(config!['playerOptions']['autoplay'], true); + expect(config['theme'], 'dark'); + }); + + test('should return null when config not available', () async { + final mockClient = MockClient((request) async { + return http.Response('Not Found', 404); + }); + + final config = await _testGetPlayerConfig( + client: mockClient, + playerType: 'nonexistent', + ); + + expect(config, isNull); + }); + }); + + group('Server Health Check', () { + test('should return true when server is available', () async { + final mockClient = MockClient((request) async { + if (request.url.path == '/api/player/health') { + return http.Response( + jsonEncode({'status': 'ok', 'version': '1.0.0'}), + 200, + ); + } + return http.Response('Not Found', 404); + }); + + final isAvailable = await _testIsServerApiAvailable(mockClient); + expect(isAvailable, true); + }); + + test('should return false when server is unavailable', () async { + final mockClient = MockClient((request) async { + return http.Response('Server Error', 500); + }); + + final isAvailable = await _testIsServerApiAvailable(mockClient); + expect(isAvailable, false); + }); + + test('should return false on network timeout', () async { + final mockClient = MockClient((request) async { + throw const SocketException('Connection timeout'); + }); + + final isAvailable = await _testIsServerApiAvailable(mockClient); + expect(isAvailable, false); + }); + }); + + group('URL Encoding', () { + test('should properly encode special characters in video URL', () async { + final mockClient = MockClient((request) async { + return http.Response('Server Error', 500); // Force fallback + }); + + final embedUrl = await _testGetVibixEmbedUrl( + client: mockClient, + videoUrl: 'http://example.com/path with spaces/movie&test.mp4', + title: 'Movie Title (2023)', + ); + + expect(embedUrl, contains('path%20with%20spaces')); + expect(embedUrl, contains('movie%26test.mp4')); + expect(embedUrl, contains('Movie%20Title%20%282023%29')); + }); + + test('should handle non-ASCII characters in title', () async { + final mockClient = MockClient((request) async { + return http.Response('Server Error', 500); // Force fallback + }); + + final embedUrl = await _testGetVibixEmbedUrl( + client: mockClient, + videoUrl: 'http://example.com/video.mp4', + title: 'Тест Фильм Россия', + ); + + expect(embedUrl, contains('title=%D0%A2%D0%B5%D1%81%D1%82')); + }); + }); + }); +} + +// Helper functions to test with mocked http client +// Note: In a real implementation, you would inject the http client + +Future _testGetVibixEmbedUrl({ + required http.Client client, + required String videoUrl, + required String title, + String? imdbId, + String? season, + String? episode, +}) async { + // This simulates the PlayerEmbedService.getVibixEmbedUrl behavior + // In real implementation, you'd need dependency injection for the http client + try { + final response = await client.post( + Uri.parse('https://neomovies.site/api/player/vibix/embed'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: jsonEncode({ + 'videoUrl': videoUrl, + 'title': title, + 'imdbId': imdbId, + 'season': season, + 'episode': episode, + 'autoplay': true, + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return data['embedUrl'] as String; + } else { + throw Exception('Failed to get Vibix embed URL: ${response.statusCode}'); + } + } catch (e) { + // Fallback to direct URL + final encodedVideoUrl = Uri.encodeComponent(videoUrl); + final encodedTitle = Uri.encodeComponent(title); + return 'https://vibix.me/embed/?src=$encodedVideoUrl&autoplay=1&title=$encodedTitle'; + } +} + +Future _testGetAllohaEmbedUrl({ + required http.Client client, + required String videoUrl, + required String title, + String? imdbId, + String? season, + String? episode, +}) async { + try { + final response = await client.post( + Uri.parse('https://neomovies.site/api/player/alloha/embed'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: jsonEncode({ + 'videoUrl': videoUrl, + 'title': title, + 'imdbId': imdbId, + 'season': season, + 'episode': episode, + 'autoplay': true, + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return data['embedUrl'] as String; + } else { + throw Exception('Failed to get Alloha embed URL: ${response.statusCode}'); + } + } catch (e) { + // Fallback to direct URL + final encodedVideoUrl = Uri.encodeComponent(videoUrl); + final encodedTitle = Uri.encodeComponent(title); + return 'https://alloha.tv/embed?src=$encodedVideoUrl&autoplay=1&title=$encodedTitle'; + } +} + +Future?> _testGetPlayerConfig({ + required http.Client client, + required String playerType, + String? imdbId, + String? season, + String? episode, +}) async { + try { + final response = await client.get( + Uri.parse('https://neomovies.site/api/player/$playerType/config').replace( + queryParameters: { + if (imdbId != null) 'imdbId': imdbId, + if (season != null) 'season': season, + if (episode != null) 'episode': episode, + }, + ), + headers: { + 'Accept': 'application/json', + }, + ); + + if (response.statusCode == 200) { + return jsonDecode(response.body) as Map; + } else { + return null; + } + } catch (e) { + return null; + } +} + +Future _testIsServerApiAvailable(http.Client client) async { + try { + final response = await client.get( + Uri.parse('https://neomovies.site/api/player/health'), + headers: {'Accept': 'application/json'}, + ).timeout(const Duration(seconds: 5)); + + return response.statusCode == 200; + } catch (e) { + return false; + } +} \ No newline at end of file diff --git a/test/services/torrent_platform_service_test.dart b/test/services/torrent_platform_service_test.dart new file mode 100644 index 0000000..eb108d9 --- /dev/null +++ b/test/services/torrent_platform_service_test.dart @@ -0,0 +1,331 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:neomovies_mobile/data/models/torrent_info.dart'; +import 'package:neomovies_mobile/data/services/torrent_platform_service.dart'; + +void main() { + group('TorrentPlatformService Tests', () { + late TorrentPlatformService service; + late List methodCalls; + + setUp(() { + service = TorrentPlatformService(); + methodCalls = []; + + // Mock the platform channel + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('com.neo.neomovies_mobile/torrent'), + (MethodCall methodCall) async { + methodCalls.add(methodCall); + return _handleMethodCall(methodCall); + }, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('com.neo.neomovies_mobile/torrent'), + null, + ); + }); + + group('Torrent Management', () { + test('addTorrent should call Android method with correct parameters', () async { + const magnetUri = 'magnet:?xt=urn:btih:test123&dn=test.movie.mkv'; + const downloadPath = '/storage/emulated/0/Download/Torrents'; + + await service.addTorrent(magnetUri, downloadPath); + + expect(methodCalls.length, 1); + expect(methodCalls.first.method, 'addTorrent'); + expect(methodCalls.first.arguments, { + 'magnetUri': magnetUri, + 'downloadPath': downloadPath, + }); + }); + + test('removeTorrent should call Android method with torrent hash', () async { + const torrentHash = 'abc123def456'; + + await service.removeTorrent(torrentHash); + + expect(methodCalls.length, 1); + expect(methodCalls.first.method, 'removeTorrent'); + expect(methodCalls.first.arguments, {'torrentHash': torrentHash}); + }); + + test('pauseTorrent should call Android method with torrent hash', () async { + const torrentHash = 'abc123def456'; + + await service.pauseTorrent(torrentHash); + + expect(methodCalls.length, 1); + expect(methodCalls.first.method, 'pauseTorrent'); + expect(methodCalls.first.arguments, {'torrentHash': torrentHash}); + }); + + test('resumeTorrent should call Android method with torrent hash', () async { + const torrentHash = 'abc123def456'; + + await service.resumeTorrent(torrentHash); + + expect(methodCalls.length, 1); + expect(methodCalls.first.method, 'resumeTorrent'); + expect(methodCalls.first.arguments, {'torrentHash': torrentHash}); + }); + }); + + group('Torrent Information', () { + test('getAllTorrents should return list of TorrentInfo objects', () async { + final torrents = await service.getAllTorrents(); + + expect(methodCalls.length, 1); + expect(methodCalls.first.method, 'getAllTorrents'); + expect(torrents, isA>()); + expect(torrents.length, 2); // Based on mock data + + final firstTorrent = torrents.first; + expect(firstTorrent.name, 'Test Movie 1080p.mkv'); + expect(firstTorrent.infoHash, 'abc123def456'); + expect(firstTorrent.state, 'downloading'); + expect(firstTorrent.progress, 0.65); + }); + + test('getTorrentInfo should return specific torrent information', () async { + const torrentHash = 'abc123def456'; + + final torrent = await service.getTorrentInfo(torrentHash); + + expect(methodCalls.length, 1); + expect(methodCalls.first.method, 'getTorrentInfo'); + expect(methodCalls.first.arguments, {'torrentHash': torrentHash}); + expect(torrent, isA()); + expect(torrent?.infoHash, torrentHash); + }); + }); + + group('File Priority Management', () { + test('setFilePriority should call Android method with correct parameters', () async { + const torrentHash = 'abc123def456'; + const fileIndex = 0; + const priority = FilePriority.high; + + await service.setFilePriority(torrentHash, fileIndex, priority); + + expect(methodCalls.length, 1); + expect(methodCalls.first.method, 'setFilePriority'); + expect(methodCalls.first.arguments, { + 'torrentHash': torrentHash, + 'fileIndex': fileIndex, + 'priority': priority.value, + }); + }); + + test('getFilePriorities should return list of priorities', () async { + const torrentHash = 'abc123def456'; + + final priorities = await service.getFilePriorities(torrentHash); + + expect(methodCalls.length, 1); + expect(methodCalls.first.method, 'getFilePriorities'); + expect(methodCalls.first.arguments, {'torrentHash': torrentHash}); + expect(priorities, isA>()); + expect(priorities.length, 3); // Based on mock data + }); + }); + + group('Error Handling', () { + test('should handle PlatformException gracefully', () async { + // Override mock to throw exception + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('com.neo.neomovies_mobile/torrent'), + (MethodCall methodCall) async { + throw PlatformException( + code: 'TORRENT_ERROR', + message: 'Failed to add torrent', + details: 'Invalid magnet URI', + ); + }, + ); + + expect( + () => service.addTorrent('invalid-magnet', '/path'), + throwsA(isA()), + ); + }); + + test('should handle null response from platform', () async { + // Override mock to return null + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('com.neo.neomovies_mobile/torrent'), + (MethodCall methodCall) async => null, + ); + + final result = await service.getTorrentInfo('nonexistent'); + expect(result, isNull); + }); + }); + + group('State Management', () { + test('torrent states should be correctly identified', () async { + final torrents = await service.getAllTorrents(); + + // Find torrents with different states + final downloadingTorrent = torrents.firstWhere( + (t) => t.state == 'downloading', + ); + final seedingTorrent = torrents.firstWhere( + (t) => t.state == 'seeding', + ); + + expect(downloadingTorrent.isDownloading, isTrue); + expect(downloadingTorrent.isSeeding, isFalse); + expect(downloadingTorrent.isCompleted, isFalse); + + expect(seedingTorrent.isDownloading, isFalse); + expect(seedingTorrent.isSeeding, isTrue); + expect(seedingTorrent.isCompleted, isTrue); + }); + + test('progress calculation should be accurate', () async { + final torrents = await service.getAllTorrents(); + final torrent = torrents.first; + + expect(torrent.progress, inInclusiveRange(0.0, 1.0)); + expect(torrent.formattedProgress, '65%'); + }); + }); + + group('Video File Detection', () { + test('should identify video files correctly', () async { + final torrents = await service.getAllTorrents(); + final torrent = torrents.first; + + final videoFiles = torrent.videoFiles; + expect(videoFiles.isNotEmpty, isTrue); + + final videoFile = videoFiles.first; + expect(videoFile.name.toLowerCase(), contains('.mkv')); + expect(videoFile.isVideo, isTrue); + }); + + test('should find main video file', () async { + final torrents = await service.getAllTorrents(); + final torrent = torrents.first; + + final mainFile = torrent.mainVideoFile; + expect(mainFile, isNotNull); + expect(mainFile!.isVideo, isTrue); + expect(mainFile.size, greaterThan(0)); + }); + }); + }); +} + +/// Mock method call handler for torrent platform channel +dynamic _handleMethodCall(MethodCall methodCall) { + switch (methodCall.method) { + case 'addTorrent': + return {'success': true, 'torrentHash': 'abc123def456'}; + + case 'removeTorrent': + case 'pauseTorrent': + case 'resumeTorrent': + return {'success': true}; + + case 'getAllTorrents': + return _getMockTorrentsData(); + + case 'getTorrentInfo': + final hash = methodCall.arguments['torrentHash'] as String; + final torrents = _getMockTorrentsData(); + return torrents.firstWhere( + (t) => t['infoHash'] == hash, + orElse: () => null, + ); + + case 'setFilePriority': + return {'success': true}; + + case 'getFilePriorities': + return [ + FilePriority.high.value, + FilePriority.normal.value, + FilePriority.low.value, + ]; + + default: + throw PlatformException( + code: 'UNIMPLEMENTED', + message: 'Method ${methodCall.method} not implemented', + ); + } +} + +/// Mock torrents data for testing +List> _getMockTorrentsData() { + return [ + { + 'name': 'Test Movie 1080p.mkv', + 'infoHash': 'abc123def456', + 'state': 'downloading', + 'progress': 0.65, + 'downloadSpeed': 2500000, // 2.5 MB/s + 'uploadSpeed': 800000, // 800 KB/s + 'totalSize': 4294967296, // 4 GB + 'downloadedSize': 2791728742, // ~2.6 GB + 'seeders': 15, + 'leechers': 8, + 'ratio': 1.2, + 'addedTime': DateTime.now().subtract(const Duration(hours: 2)).millisecondsSinceEpoch, + 'files': [ + { + 'name': 'Test Movie 1080p.mkv', + 'size': 4294967296, + 'path': '/storage/emulated/0/Download/Torrents/Test Movie 1080p.mkv', + 'priority': FilePriority.high.value, + }, + { + 'name': 'subtitle.srt', + 'size': 65536, + 'path': '/storage/emulated/0/Download/Torrents/subtitle.srt', + 'priority': FilePriority.normal.value, + }, + { + 'name': 'NFO.txt', + 'size': 2048, + 'path': '/storage/emulated/0/Download/Torrents/NFO.txt', + 'priority': FilePriority.low.value, + }, + ], + }, + { + 'name': 'Another Movie 720p', + 'infoHash': 'def456ghi789', + 'state': 'seeding', + 'progress': 1.0, + 'downloadSpeed': 0, + 'uploadSpeed': 500000, // 500 KB/s + 'totalSize': 2147483648, // 2 GB + 'downloadedSize': 2147483648, + 'seeders': 25, + 'leechers': 3, + 'ratio': 2.5, + 'addedTime': DateTime.now().subtract(const Duration(days: 1)).millisecondsSinceEpoch, + 'files': [ + { + 'name': 'Another Movie 720p.mp4', + 'size': 2147483648, + 'path': '/storage/emulated/0/Download/Torrents/Another Movie 720p.mp4', + 'priority': FilePriority.high.value, + }, + ], + }, + ]; +} \ No newline at end of file