import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../services/xtream_api.dart'; import '../models/xtream_models.dart'; enum ContentType { live, movies, series } // Special category constants class SpecialCategories { static const String argentineFootball = 'Fútbol Argentino'; } class IPTVProvider extends ChangeNotifier { final XtreamApiService _api = XtreamApiService(); bool _isLoading = false; String? _error; XtreamUserInfo? _userInfo; int _loadedChannels = 0; int _totalChannels = 0; List _liveCategories = []; List _vodCategories = []; List _seriesCategories = []; List _liveStreams = []; List _vodStreams = []; List _seriesList = []; List _seriesEpisodes = []; String _selectedLiveCategory = ''; String _selectedVodCategory = ''; String _selectedCountry = ''; String _selectedCategory = ''; // For special categories like "Fútbol Argentino" List _countries = []; bool _isOrganizingCountries = false; XtreamSeries? _selectedSeries; bool get isLoading => _isLoading; String? get error => _error; XtreamUserInfo? get userInfo => _userInfo; XtreamApiService get api => _api; int get loadedChannels => _loadedChannels; int get totalChannels => _totalChannels; double get loadingProgress => _totalChannels > 0 ? _loadedChannels / _totalChannels : 0.0; bool get isOrganizingCountries => _isOrganizingCountries; List get liveCategories => _liveCategories; List get vodCategories => _vodCategories; List get seriesCategories => _seriesCategories; List get liveStreams => _liveStreams; List get vodStreams => _vodStreams; List get seriesList => _seriesList; List get seriesEpisodes => _seriesEpisodes; String get selectedLiveCategory => _selectedLiveCategory; String get selectedCountry => _selectedCountry; String get selectedCategory => _selectedCategory; List get countries { print('DEBUG: ========================================='); print('DEBUG: countries getter called'); print('DEBUG: _countries list length: ${_countries.length}'); print('DEBUG: _countries list content: $_countries'); print('DEBUG: _countries is empty: ${_countries.isEmpty}'); print('DEBUG: ========================================='); return _countries; } /// Get display items for sidebar including special categories /// Returns a list of maps with 'name', 'type', and 'priority' for proper ordering List> get sidebarItems { final items = >[]; // Add all countries with their priority for (final country in _countries) { int priority = _getCountryPriority(country); items.add({ 'name': country, 'type': 'country', 'priority': priority, }); } // Add special category: Fútbol Argentino (priority 2.5 - between Perú and other countries) // Only add if there are any Argentine football channels final hasArgentineFootball = _liveStreams.any((s) => _api.isArgentineFootballChannel(s.name)); if (hasArgentineFootball) { items.add({ 'name': SpecialCategories.argentineFootball, 'type': 'category', 'priority': 2.5, // Between Perú (2) and other South American countries (3) }); } // Sort by priority, then alphabetically items.sort((a, b) { final priorityCompare = (a['priority'] as double).compareTo(b['priority'] as double); if (priorityCompare != 0) return priorityCompare; return (a['name'] as String).compareTo(b['name'] as String); }); return items; } /// Get priority for a country (lower number = higher priority) int _getCountryPriority(String country) { switch (country) { case 'Argentina': return 1; case 'Perú': case 'Peru': return 2; case 'Bolivia': case 'Brasil': case 'Brazil': case 'Chile': case 'Colombia': case 'Ecuador': case 'Paraguay': case 'Uruguay': case 'Venezuela': return 3; default: return 100; // Low priority for other countries } } XtreamSeries? get selectedSeries => _selectedSeries; Future login(String server, String username, String password) async { _isLoading = true; _error = null; notifyListeners(); try { _api.setCredentials(server, username, password); _userInfo = await _api.getUserInfo(); // No automatic data loading on startup - data loads on demand only await _saveCredentials(server, username, password); } catch (e) { _error = e.toString(); } _isLoading = false; notifyListeners(); } Future _loadCategories() async { try { _liveCategories = await _api.getLiveCategories(); _vodCategories = await _api.getVodCategories(); _seriesCategories = await _api.getSeriesCategories(); } catch (e) { _error = e.toString(); } } Future loadLiveStreams([String categoryId = '']) async { print('DEBUG: ========================================================='); print('DEBUG: loadLiveStreams() START - API First Strategy'); print('DEBUG: ========================================================='); _isLoading = true; _isOrganizingCountries = false; _loadedChannels = 0; _totalChannels = 0; _countries = []; notifyListeners(); try { // STEP 1: Load from API first (much faster than M3U) print('DEBUG: Attempting to load from API first...'); try { _liveStreams = await _api.getLiveStreams(categoryId); _selectedLiveCategory = categoryId; _totalChannels = _liveStreams.length; _loadedChannels = _liveStreams.length; print('DEBUG: API SUCCESS - Loaded ${_liveStreams.length} streams in < 5 seconds'); if (_liveStreams.isEmpty) { throw Exception('API returned 0 streams'); } } catch (apiError) { print('DEBUG: API failed: $apiError'); print('DEBUG: Falling back to M3U...'); // Fallback to M3U only if API fails _liveStreams = await _api.getM3UStreams( onProgress: (loaded, total) { _loadedChannels = loaded; _totalChannels = total; print('DEBUG: M3U progress: $loaded of $total'); notifyListeners(); }, ); _selectedLiveCategory = categoryId; print('DEBUG: M3U FALLBACK - Loaded ${_liveStreams.length} streams'); if (_liveStreams.isEmpty) { throw Exception('No channels available from API or M3U'); } } // STEP 2: Mark loading complete - channels ready to display print('DEBUG: === CHANNELS READY - Starting background country extraction ==='); _isLoading = false; notifyListeners(); // STEP 3: Extract countries in background (using optimized method) _extractCountriesInBackground(); } catch (e) { _error = e.toString(); print('DEBUG: ERROR loading streams: $e'); } print('DEBUG: ========================================================='); print('DEBUG: loadLiveStreams() END - Loaded ${_liveStreams.length} channels'); print('DEBUG: ========================================================='); _isLoading = false; notifyListeners(); } /// Extract countries from streams in the background to avoid UI freezing void _extractCountriesInBackground() { if (_liveStreams.isEmpty) return; _isOrganizingCountries = true; notifyListeners(); print('DEBUG: Starting background country extraction from ${_liveStreams.length} streams...'); // Use Future.microtask to schedule the extraction after the current frame Future.microtask(() { try { // Use optimized extraction (only sample 2000 channels for speed) _countries = _api.getCountriesOptimized(_liveStreams, maxChannelsToProcess: 2000); print('DEBUG: Countries extraction complete. Found ${_countries.length} countries'); print('DEBUG: Countries list: $_countries'); } catch (e) { print('DEBUG: Error extracting countries: $e'); _countries = []; } finally { _isOrganizingCountries = false; print('DEBUG: === CHANNEL LOADING COMPLETE ==='); notifyListeners(); } }); } // Extract country names from live categories (format: "Country|XX") List _extractCountriesFromCategories() { final countries = {}; for (final category in _liveCategories) { final countryName = category.name.split('|').first.trim(); // Only add if it's a valid country (not a group title) if (countryName.isNotEmpty && !_isGroupTitle(countryName)) { countries.add(countryName); } } return countries.toList()..sort(); } /// Check if a string is a group title (not a country) bool _isGroupTitle(String name) { final normalized = name.toLowerCase().trim(); final groupTitles = { '24/7', '24/7 ar', '24/7 in', '24/7-es', '24/7-de', '24/7-gr', '24/7-my', '24/7-pt', '24/7-ro', '24/7-tr', '24/7-latino', 'vip', 'vip - pk', 'ppv', 'movies', 'cine', 'cine sd', 'cine y serie', 'latino', 'general', 'music', 'religious', 'bein', 'mbc', 'tod', 'osn', 'myhd', 'dstv', 'art', 'icc-ca', 'icc-car', 'icc-dstv', 'icc-in', 'icc-nz', 'icc-pk', 'icc-uk', 'xmas', 'sin', 'ezd', 'exyu', 'rot', 'ar-kids', 'ar-sp', 'islam', 'bab', 'as', 'ei' }; return groupTitles.contains(normalized); } // Build a map from category ID to country name for API streams Map _buildCategoryToCountryMap() { final map = {}; for (final category in _liveCategories) { final countryName = category.name.split('|').first.trim(); // Only add if it's a valid country (not a group title) if (countryName.isNotEmpty && !_isGroupTitle(countryName)) { map[category.id] = countryName; } } print('DEBUG: Built category map with ${map.length} entries'); return map; } void filterByCountry(String country) { _selectedCountry = country.trim(); _selectedCategory = ''; // Clear special category when country is selected print('DEBUG: Filter by country: "$_selectedCountry"'); notifyListeners(); } void filterByCategory(String category) { _selectedCategory = category.trim(); _selectedCountry = ''; // Clear country when special category is selected print('DEBUG: Filter by category: "$_selectedCategory"'); notifyListeners(); } List get filteredLiveStreams { // If a special category is selected, filter by that if (_selectedCategory.isNotEmpty) { print('DEBUG: Filtering by special category: "$_selectedCategory"'); return _api.filterByCategory(_liveStreams, _selectedCategory); } // Show all if empty or "Todos"/"All" selected final normalizedCountry = _selectedCountry.trim(); if (normalizedCountry.isEmpty || normalizedCountry.toLowerCase() == 'todos' || normalizedCountry.toLowerCase() == 'all') { return _liveStreams; } // Build category map for API streams that don't have country in name final categoryMap = _buildCategoryToCountryMap(); return _api.filterByCountry(_liveStreams, _selectedCountry, categoryToCountryMap: categoryMap); } Future loadVodStreams([String categoryId = '']) async { _isLoading = true; notifyListeners(); try { _vodStreams = await _api.getVodStreams(categoryId); _selectedVodCategory = categoryId; } catch (e) { _error = e.toString(); } _isLoading = false; notifyListeners(); } Future loadSeries() async { _isLoading = true; notifyListeners(); try { _seriesList = await _api.getSeries(); } catch (e) { _error = e.toString(); } _isLoading = false; notifyListeners(); } Future loadSeriesEpisodes(XtreamSeries series) async { _isLoading = true; notifyListeners(); try { _selectedSeries = series; _seriesEpisodes = await _api.getSeriesEpisodes(series.seriesId); } catch (e) { _error = e.toString(); } _isLoading = false; notifyListeners(); } Future reloadM3UStreams() async { _isLoading = true; _isOrganizingCountries = false; _error = null; _loadedChannels = 0; _totalChannels = 0; _countries = []; notifyListeners(); try { // Try API first, then M3U fallback try { print('DEBUG: Attempting to reload from API...'); _liveStreams = await _api.getLiveStreams(''); _totalChannels = _liveStreams.length; _loadedChannels = _liveStreams.length; print('DEBUG: API reload - Loaded ${_liveStreams.length} streams'); } catch (apiError) { print('DEBUG: API reload failed: $apiError'); print('DEBUG: Falling back to M3U...'); _liveStreams = await _api.getM3UStreams( onProgress: (loaded, total) { _loadedChannels = loaded; _totalChannels = total; print('DEBUG: M3U progress: $loaded of $total'); notifyListeners(); }, ); print('DEBUG: M3U reload - Loaded ${_liveStreams.length} streams'); } // Mark loading as complete - channels are ready to display _isLoading = false; notifyListeners(); // Extract countries in background (optimized) _extractCountriesInBackground(); } catch (e) { print('DEBUG: Error reloading channels: $e'); _error = 'Error al cargar canales: $e'; _isLoading = false; _isOrganizingCountries = false; notifyListeners(); } } /// Downloads M3U playlist and saves it as JSON file /// Returns the file path where the JSON was saved Future downloadAndSaveM3UAsJson() async { _isLoading = true; _error = null; notifyListeners(); try { print('DEBUG: Starting M3U download and JSON conversion...'); // If we already have streams loaded, save those instead of downloading again if (_liveStreams.isNotEmpty) { print('DEBUG: Using already loaded ${_liveStreams.length} streams'); // Create M3U result from loaded streams final channels = _liveStreams.map((stream) => M3UChannel( name: stream.name, url: stream.url ?? '', groupTitle: stream.plot ?? 'Unknown', tvgLogo: stream.streamIcon, )).toList(); final result = M3UDownloadResult( sourceUrl: '${_api.server}/get.php', downloadTime: DateTime.now(), totalChannels: channels.length, groupsCount: _groupChannelsByCountry(channels), channels: channels, ); // Save as JSON file final filePath = await _api.saveM3UAsJson(result); print('DEBUG: Saved JSON to: $filePath'); _isLoading = false; notifyListeners(); return filePath; } // If no streams loaded, try to download print('DEBUG: No streams loaded, attempting download...'); final result = await _api.downloadM3UAsJson(); print('DEBUG: Downloaded ${result.totalChannels} channels from ${result.sourceUrl}'); print('DEBUG: Groups found: ${result.groupsCount}'); // Save as JSON file final filePath = await _api.saveM3UAsJson(result); print('DEBUG: Saved JSON to: $filePath'); _isLoading = false; notifyListeners(); return filePath; } catch (e) { print('DEBUG: Error downloading/saving M3U as JSON: $e'); _error = 'Error al descargar playlist: $e'; _isLoading = false; notifyListeners(); throw Exception(_error); } } Map _groupChannelsByCountry(List channels) { final groups = {}; for (final channel in channels) { final country = channel.groupTitle ?? 'Unknown'; groups[country] = (groups[country] ?? 0) + 1; } return groups; } /// Saves all loaded live channels as a text file for analysis Future saveChannelsAsText() async { try { print('DEBUG: Saving ${_liveStreams.length} channels as text file'); // Build text content final buffer = StringBuffer(); buffer.writeln('=== XSTREAM TV - LISTA DE CANALES ==='); buffer.writeln('Fecha: ${DateTime.now()}'); buffer.writeln('Total de canales: ${_liveStreams.length}'); buffer.writeln(''); // Build category map for API streams final categoryMap = _buildCategoryToCountryMap(); // Group by country final groupedChannels = >{}; for (final stream in _liveStreams) { String? country; // First try to extract from name (M3U format) final countryFromName = _api.extractCountryFromChannelName(stream.name); if (countryFromName.isNotEmpty) { country = countryFromName; } // If not found, try category mapping (API format) else if (stream.categoryId != null && categoryMap.containsKey(stream.categoryId)) { country = categoryMap[stream.categoryId]; } final normalizedCountry = country != null && country.isNotEmpty ? _api.normalizeCountry(country) : 'Sin País'; if (!groupedChannels.containsKey(normalizedCountry)) { groupedChannels[normalizedCountry] = []; } groupedChannels[normalizedCountry]!.add(stream); } // Write grouped channels final sortedCountries = groupedChannels.keys.toList()..sort(); for (final country in sortedCountries) { final channels = groupedChannels[country]!; buffer.writeln(''); buffer.writeln('=== $country (${channels.length} canales) ==='); buffer.writeln(''); for (int i = 0; i < channels.length; i++) { final stream = channels[i]; buffer.writeln('${i + 1}. ${stream.name}'); if (stream.url != null && stream.url!.isNotEmpty) { buffer.writeln(' URL: ${stream.url}'); } } } // Save to file final fileName = 'xstream_canales_${DateTime.now().millisecondsSinceEpoch}.txt'; final filePath = await _api.saveTextFile(fileName, buffer.toString()); print('DEBUG: Saved channels list to: $filePath'); return filePath; } catch (e) { print('DEBUG: Error saving channels as text: $e'); throw Exception('Error al guardar lista: $e'); } } void clearError() { _error = null; notifyListeners(); } Future _saveCredentials(String server, String username, String password) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('server', server); await prefs.setString('username', username); await prefs.setString('password', password); } Future loadSavedCredentials() async { final prefs = await SharedPreferences.getInstance(); final server = prefs.getString('server'); final username = prefs.getString('username'); final password = prefs.getString('password'); if (server != null && username != null && password != null) { await login(server, username, password); return true; } return false; } Future logout() async { final prefs = await SharedPreferences.getInstance(); await prefs.remove('server'); await prefs.remove('username'); await prefs.remove('password'); _userInfo = null; _liveCategories = []; _vodCategories = []; _seriesCategories = []; _liveStreams = []; _vodStreams = []; _seriesList = []; _countries = []; _selectedCategory = ''; _isOrganizingCountries = false; notifyListeners(); } }