import 'dart:convert'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:http/http.dart' as http; import 'package:neomovies_mobile/data/models/auth_response.dart'; import 'package:neomovies_mobile/data/models/favorite.dart'; import 'package:neomovies_mobile/data/models/movie.dart'; import 'package:neomovies_mobile/data/models/reaction.dart'; import 'package:neomovies_mobile/data/models/user.dart'; import 'package:neomovies_mobile/data/models/torrent.dart'; import 'package:neomovies_mobile/data/models/torrent/torrent_item.dart'; import 'package:neomovies_mobile/data/models/player/player_response.dart'; /// New API client for neomovies-api (Go-based backend) /// This client provides improved performance and new features: /// - Email verification flow /// - Google OAuth support /// - Torrent search via RedAPI /// - Multiple player support (Alloha, Lumex, Vibix) /// - Enhanced reactions system class NeoMoviesApiClient { final http.Client _client; final String _baseUrl; final String _apiVersion = 'v1'; NeoMoviesApiClient(this._client, {String? baseUrl}) : _baseUrl = baseUrl ?? dotenv.env['API_URL'] ?? 'https://api.neomovies.ru'; String get apiUrl => '$_baseUrl/api/$_apiVersion'; // ============================================ // Authentication Endpoints // ============================================ /// Register a new user (sends verification code to email) /// Returns: {"success": true, "message": "Verification code sent"} Future> register({ required String email, required String password, required String name, }) async { final uri = Uri.parse('$apiUrl/auth/register'); final response = await _client.post( uri, headers: {'Content-Type': 'application/json'}, body: json.encode({ 'email': email, 'password': password, 'name': name, }), ); if (response.statusCode == 200 || response.statusCode == 201) { return json.decode(response.body); } else { throw Exception('Registration failed: ${response.body}'); } } /// Verify email with code sent during registration /// Returns: AuthResponse with JWT token and user info Future verifyEmail({ required String email, required String code, }) async { final uri = Uri.parse('$apiUrl/auth/verify'); final response = await _client.post( uri, headers: {'Content-Type': 'application/json'}, body: json.encode({ 'email': email, 'code': code, }), ); if (response.statusCode == 200) { return AuthResponse.fromJson(json.decode(response.body)); } else { throw Exception('Verification failed: ${response.body}'); } } /// Resend verification code to email Future resendVerificationCode(String email) async { final uri = Uri.parse('$apiUrl/auth/resend-code'); final response = await _client.post( uri, headers: {'Content-Type': 'application/json'}, body: json.encode({'email': email}), ); if (response.statusCode != 200) { throw Exception('Failed to resend code: ${response.body}'); } } /// Login with email and password Future login({ required String email, required String password, }) async { final uri = Uri.parse('$apiUrl/auth/login'); final response = await _client.post( uri, headers: {'Content-Type': 'application/json'}, body: json.encode({ 'email': email, 'password': password, }), ); if (response.statusCode == 200) { return AuthResponse.fromJson(json.decode(response.body)); } else { throw Exception('Login failed: ${response.body}'); } } /// Get Google OAuth login URL /// User should be redirected to this URL in a WebView String getGoogleOAuthUrl() { return '$apiUrl/auth/google/login'; } /// Refresh authentication token Future refreshToken(String refreshToken) async { final uri = Uri.parse('$apiUrl/auth/refresh'); final response = await _client.post( uri, headers: {'Content-Type': 'application/json'}, body: json.encode({'refreshToken': refreshToken}), ); if (response.statusCode == 200) { return AuthResponse.fromJson(json.decode(response.body)); } else { throw Exception('Token refresh failed: ${response.body}'); } } /// Get current user profile Future getProfile() async { final uri = Uri.parse('$apiUrl/auth/profile'); final response = await _client.get(uri); if (response.statusCode == 200) { return User.fromJson(json.decode(response.body)); } else { throw Exception('Failed to get profile: ${response.body}'); } } /// Delete user account Future deleteAccount() async { final uri = Uri.parse('$apiUrl/auth/profile'); final response = await _client.delete(uri); if (response.statusCode != 200) { throw Exception('Failed to delete account: ${response.body}'); } } // ============================================ // Movies Endpoints // ============================================ /// Get popular movies Future> getPopularMovies({int page = 1}) async { return _fetchMovies('/movies/popular', page: page); } /// Get top rated movies Future> getTopRatedMovies({int page = 1}) async { return _fetchMovies('/movies/top-rated', page: page); } /// Get upcoming movies Future> getUpcomingMovies({int page = 1}) async { return _fetchMovies('/movies/upcoming', page: page); } /// Get now playing movies Future> getNowPlayingMovies({int page = 1}) async { return _fetchMovies('/movies/now-playing', page: page); } /// Get movie by ID Future getMovieById(String id) async { final uri = Uri.parse('$apiUrl/movies/$id'); print('Fetching movie from: $uri'); final response = await _client.get(uri); print('Response status: ${response.statusCode}'); print('Response body: ${response.body.substring(0, response.body.length > 500 ? 500 : response.body.length)}...'); if (response.statusCode == 200) { final apiResponse = json.decode(response.body); print('Decoded API response type: ${apiResponse.runtimeType}'); print('API response keys: ${apiResponse is Map ? apiResponse.keys.toList() : 'Not a map'}'); // API returns: {"success": true, "data": {...}} final movieData = (apiResponse is Map && apiResponse['data'] != null) ? apiResponse['data'] : apiResponse; print('Movie data keys: ${movieData is Map ? movieData.keys.toList() : 'Not a map'}'); print('Movie data: $movieData'); return Movie.fromJson(movieData); } else { throw Exception('Failed to load movie: ${response.statusCode} - ${response.body}'); } } /// Get movie recommendations Future> getMovieRecommendations(String movieId, {int page = 1}) async { return _fetchMovies('/movies/$movieId/recommendations', page: page); } /// Search movies Future> searchMovies(String query, {int page = 1}) async { return _fetchMovies('/movies/search', page: page, query: query); } // ============================================ // TV Shows Endpoints // ============================================ /// Get popular TV shows Future> getPopularTvShows({int page = 1}) async { return _fetchMovies('/tv/popular', page: page); } /// Get top rated TV shows Future> getTopRatedTvShows({int page = 1}) async { return _fetchMovies('/tv/top-rated', page: page); } /// Get TV show by ID Future getTvShowById(String id) async { final uri = Uri.parse('$apiUrl/tv/$id'); print('Fetching TV show from: $uri'); final response = await _client.get(uri); print('Response status: ${response.statusCode}'); print('Response body: ${response.body.substring(0, response.body.length > 500 ? 500 : response.body.length)}...'); if (response.statusCode == 200) { final apiResponse = json.decode(response.body); print('Decoded API response type: ${apiResponse.runtimeType}'); print('API response keys: ${apiResponse is Map ? apiResponse.keys.toList() : 'Not a map'}'); // API returns: {"success": true, "data": {...}} final tvData = (apiResponse is Map && apiResponse['data'] != null) ? apiResponse['data'] : apiResponse; print('TV data keys: ${tvData is Map ? tvData.keys.toList() : 'Not a map'}'); print('TV data: $tvData'); return Movie.fromJson(tvData); } else { throw Exception('Failed to load TV show: ${response.statusCode} - ${response.body}'); } } /// Get TV show recommendations Future> getTvShowRecommendations(String tvId, {int page = 1}) async { return _fetchMovies('/tv/$tvId/recommendations', page: page); } /// Search TV shows Future> searchTvShows(String query, {int page = 1}) async { return _fetchMovies('/tv/search', page: page, query: query); } // ============================================ // External IDs (IMDb, TVDB, etc.) // ============================================ /// Get external IDs (IMDb, TVDB) for a movie or TV show Future getExternalIds(String mediaId, String mediaType) async { try { final uri = Uri.parse('$apiUrl/${mediaType}s/$mediaId/external-ids'); final response = await _client.get(uri); if (response.statusCode == 200) { final apiResponse = json.decode(response.body); final data = (apiResponse is Map && apiResponse['data'] != null) ? apiResponse['data'] : apiResponse; return data['imdb_id'] as String?; } return null; } catch (e) { print('Error getting external IDs: $e'); return null; } } // ============================================ // Unified Search // ============================================ /// Search both movies and TV shows Future> search(String query, {int page = 1}) async { final results = await Future.wait([ searchMovies(query, page: page), searchTvShows(query, page: page), ]); // Combine and return return [...results[0], ...results[1]]; } // ============================================ // Favorites Endpoints // ============================================ /// Get user's favorite movies/shows Future> getFavorites() async { final uri = Uri.parse('$apiUrl/favorites'); final response = await _client.get(uri); if (response.statusCode == 200) { final apiResponse = json.decode(response.body); // API returns: {"success": true, "data": [...]} final List data = (apiResponse is Map && apiResponse['data'] != null) ? (apiResponse['data'] is List ? apiResponse['data'] : []) : (apiResponse is List ? apiResponse : []); return data.map((json) => Favorite.fromJson(json)).toList(); } else { throw Exception('Failed to fetch favorites: ${response.body}'); } } /// Add movie/show to favorites /// Backend automatically fetches title and poster_path from TMDB Future addFavorite({ required String mediaId, required String mediaType, required String title, required String posterPath, }) async { // Backend route: POST /favorites/{id}?type={mediaType} final uri = Uri.parse('$apiUrl/favorites/$mediaId') .replace(queryParameters: {'type': mediaType}); final response = await _client.post(uri); if (response.statusCode != 200 && response.statusCode != 201) { throw Exception('Failed to add favorite: ${response.body}'); } } /// Remove movie/show from favorites Future removeFavorite(String mediaId, {String mediaType = 'movie'}) async { // Backend route: DELETE /favorites/{id}?type={mediaType} final uri = Uri.parse('$apiUrl/favorites/$mediaId') .replace(queryParameters: {'type': mediaType}); final response = await _client.delete(uri); if (response.statusCode != 200 && response.statusCode != 204) { throw Exception('Failed to remove favorite: ${response.body}'); } } /// Check if media is in favorites Future checkIsFavorite(String mediaId, {String mediaType = 'movie'}) async { // Backend route: GET /favorites/{id}/check?type={mediaType} final uri = Uri.parse('$apiUrl/favorites/$mediaId/check') .replace(queryParameters: {'type': mediaType}); final response = await _client.get(uri); if (response.statusCode == 200) { final apiResponse = json.decode(response.body); // API returns: {"success": true, "data": {"isFavorite": true}} if (apiResponse is Map && apiResponse['data'] != null) { final data = apiResponse['data']; return data['isFavorite'] ?? false; } return false; } else { throw Exception('Failed to check favorite status: ${response.body}'); } } // ============================================ // Reactions Endpoints (NEW!) // ============================================ /// Get reaction counts for a movie/show Future> getReactionCounts({ required String mediaType, required String mediaId, }) async { final uri = Uri.parse('$apiUrl/reactions/$mediaType/$mediaId/counts'); final response = await _client.get(uri); if (response.statusCode == 200) { final data = json.decode(response.body); return Map.from(data); } else { throw Exception('Failed to get reactions: ${response.body}'); } } /// Add or update user's reaction Future setReaction({ required String mediaType, required String mediaId, required String reactionType, // 'like' or 'dislike' }) async { final uri = Uri.parse('$apiUrl/reactions/$mediaType/$mediaId'); final response = await _client.post( uri, headers: {'Content-Type': 'application/json'}, body: json.encode({'type': reactionType}), ); if (response.statusCode != 200 && response.statusCode != 201) { throw Exception('Failed to set reaction: ${response.body}'); } } /// Get user's own reactions Future> getMyReactions() async { final uri = Uri.parse('$apiUrl/reactions/my'); final response = await _client.get(uri); if (response.statusCode == 200) { final apiResponse = json.decode(response.body); // API returns: {"success": true, "data": [...]} final List data = (apiResponse is Map && apiResponse['data'] != null) ? (apiResponse['data'] is List ? apiResponse['data'] : []) : (apiResponse is List ? apiResponse : []); return data.map((json) => UserReaction.fromJson(json)).toList(); } else { throw Exception('Failed to get my reactions: ${response.body}'); } } // ============================================ // Torrent Search Endpoints (NEW!) // ============================================ /// Search torrents for a movie/show via RedAPI /// @param imdbId - IMDb ID (e.g., "tt1234567") /// @param type - "movie" or "series" /// @param quality - "1080p", "720p", "480p", etc. Future> searchTorrents({ required String imdbId, required String type, String? quality, String? season, String? episode, }) async { final queryParams = { 'type': type, if (quality != null) 'quality': quality, if (season != null) 'season': season, if (episode != null) 'episode': episode, }; final uri = Uri.parse('$apiUrl/torrents/search/$imdbId') .replace(queryParameters: queryParams); final response = await _client.get(uri); if (response.statusCode == 200) { final List data = json.decode(response.body); return data.map((json) => TorrentItem.fromJson(json)).toList(); } else { throw Exception('Failed to search torrents: ${response.body}'); } } // ============================================ // Players Endpoints (NEW!) // ============================================ /// Get Alloha player embed URL Future getAllohaPlayer(String imdbId) async { return _getPlayer('/players/alloha/$imdbId'); } /// Get Lumex player embed URL Future getLumexPlayer(String imdbId) async { return _getPlayer('/players/lumex/$imdbId'); } /// Get Vibix player embed URL Future getVibixPlayer(String imdbId) async { return _getPlayer('/players/vibix/$imdbId'); } // ============================================ // Private Helper Methods // ============================================ /// Generic method to fetch movies/TV shows Future> _fetchMovies( String endpoint, { int page = 1, String? query, }) async { final queryParams = { 'page': page.toString(), if (query != null && query.isNotEmpty) 'query': query, }; final uri = Uri.parse('$apiUrl$endpoint').replace(queryParameters: queryParams); final response = await _client.get(uri); if (response.statusCode == 200) { final decoded = json.decode(response.body); // API returns: {"success": true, "data": {"page": 1, "results": [...], ...}} List results; if (decoded is Map && decoded['success'] == true && decoded['data'] != null) { final data = decoded['data']; if (data is Map && data['results'] != null) { results = data['results']; } else if (data is List) { results = data; } else { throw Exception('Unexpected data format in API response'); } } else if (decoded is List) { results = decoded; } else if (decoded is Map && decoded['results'] != null) { results = decoded['results']; } else { throw Exception('Unexpected response format'); } return results.map((json) => Movie.fromJson(json)).toList(); } else { throw Exception('Failed to load from $endpoint: ${response.statusCode}'); } } /// Generic method to fetch player info Future _getPlayer(String endpoint) async { final uri = Uri.parse('$apiUrl$endpoint'); final response = await _client.get(uri); if (response.statusCode == 200) { return PlayerResponse.fromJson(json.decode(response.body)); } else { throw Exception('Failed to get player: ${response.body}'); } } }