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 _selectedCountry = ''; String _selectedCategory = ''; // For special categories like "Fútbol Argentino" List _countries = []; bool _isOrganizingCountries = false; XtreamSeries? _selectedSeries; Map? _categoryToCountryMapCache; List? _filteredLiveStreamsCache; String _filteredCountryCacheKey = ''; String _filteredCategoryCacheKey = ''; int _liveStreamsVersion = 0; int _filteredCacheVersion = -1; int _lastProgressUiUpdateMs = 0; 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 => _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; void _invalidateLiveDerivedCaches() { _categoryToCountryMapCache = null; _filteredLiveStreamsCache = null; _filteredCacheVersion = -1; } void _setLiveStreams(List streams, String categoryId) { _liveStreams = streams; _selectedLiveCategory = categoryId; _liveStreamsVersion++; _invalidateLiveDerivedCaches(); } void _notifyProgressUpdate() { final nowMs = DateTime.now().millisecondsSinceEpoch; final shouldUpdate = (nowMs - _lastProgressUiUpdateMs) >= 120 || (_totalChannels > 0 && _loadedChannels >= _totalChannels); if (shouldUpdate) { _lastProgressUiUpdateMs = nowMs; notifyListeners(); } } 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 loadLiveStreams([String categoryId = '']) async { _isLoading = true; _isOrganizingCountries = false; _loadedChannels = 0; _totalChannels = 0; _countries = []; _lastProgressUiUpdateMs = 0; _invalidateLiveDerivedCaches(); notifyListeners(); try { // STEP 1: Load from API first (much faster than M3U) try { _setLiveStreams(await _api.getLiveStreams(categoryId), categoryId); _totalChannels = _liveStreams.length; _loadedChannels = _liveStreams.length; if (_liveStreams.isEmpty) { throw Exception('API returned 0 streams'); } } catch (apiError) { // Fallback to M3U only if API fails _setLiveStreams( await _api.getM3UStreams( onProgress: (loaded, total) { _loadedChannels = loaded; _totalChannels = total; _notifyProgressUpdate(); }, ), categoryId, ); if (_liveStreams.isEmpty) { throw Exception('No channels available from API or M3U'); } } // STEP 2: Mark loading complete - channels ready to display _isLoading = false; notifyListeners(); // STEP 3: Extract countries in background (using optimized method) _extractCountriesInBackground(); } catch (e) { _error = e.toString(); } _isLoading = false; notifyListeners(); } /// Extract countries from streams in the background to avoid UI freezing void _extractCountriesInBackground() { if (_liveStreams.isEmpty) return; _isOrganizingCountries = true; notifyListeners(); // 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, ); } catch (e) { _countries = []; } finally { _isOrganizingCountries = false; notifyListeners(); } }); } /// 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() { if (_categoryToCountryMapCache != null) { return _categoryToCountryMapCache!; } 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; } } _categoryToCountryMapCache = map; return map; } void filterByCountry(String country) { final normalizedCountry = country.trim(); if (_selectedCountry == normalizedCountry && _selectedCategory.isEmpty) { return; } _selectedCountry = normalizedCountry; _selectedCategory = ''; // Clear special category when country is selected _filteredLiveStreamsCache = null; notifyListeners(); } void filterByCategory(String category) { final normalizedCategory = category.trim(); if (_selectedCategory == normalizedCategory && _selectedCountry.isEmpty) { return; } _selectedCategory = normalizedCategory; _selectedCountry = ''; // Clear country when special category is selected _filteredLiveStreamsCache = null; notifyListeners(); } List get filteredLiveStreams { final selectedCountry = _selectedCountry.trim(); final selectedCategory = _selectedCategory.trim(); if (_filteredLiveStreamsCache != null && _filteredCacheVersion == _liveStreamsVersion && _filteredCountryCacheKey == selectedCountry && _filteredCategoryCacheKey == selectedCategory) { return _filteredLiveStreamsCache!; } late final List result; // If a special category is selected, filter by that if (selectedCategory.isNotEmpty) { result = _api.filterByCategory(_liveStreams, selectedCategory); } else if (selectedCountry.isEmpty || selectedCountry.toLowerCase() == 'todos' || selectedCountry.toLowerCase() == 'all') { result = _liveStreams; } else { // Build category map for API streams that don't have country in name final categoryMap = _buildCategoryToCountryMap(); result = _api.filterByCountry( _liveStreams, selectedCountry, categoryToCountryMap: categoryMap, ); } _filteredCountryCacheKey = selectedCountry; _filteredCategoryCacheKey = selectedCategory; _filteredCacheVersion = _liveStreamsVersion; _filteredLiveStreamsCache = identical(result, _liveStreams) ? _liveStreams : List.unmodifiable(result); return _filteredLiveStreamsCache!; } Future loadVodStreams([String categoryId = '']) async { _isLoading = true; notifyListeners(); try { _vodStreams = await _api.getVodStreams(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 = []; _lastProgressUiUpdateMs = 0; _invalidateLiveDerivedCaches(); notifyListeners(); try { // Try API first, then M3U fallback try { _setLiveStreams(await _api.getLiveStreams(''), ''); _totalChannels = _liveStreams.length; _loadedChannels = _liveStreams.length; } catch (apiError) { _setLiveStreams( await _api.getM3UStreams( onProgress: (loaded, total) { _loadedChannels = loaded; _totalChannels = total; _notifyProgressUpdate(); }, ), '', ); } // Mark loading as complete - channels are ready to display _isLoading = false; notifyListeners(); // Extract countries in background (optimized) _extractCountriesInBackground(); } catch (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 { // If we already have streams loaded, save those instead of downloading again if (_liveStreams.isNotEmpty) { // 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); _isLoading = false; notifyListeners(); return filePath; } // If no streams loaded, try to download final result = await _api.downloadM3UAsJson(); // Save as JSON file final filePath = await _api.saveM3UAsJson(result); _isLoading = false; notifyListeners(); return filePath; } catch (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 { // 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()); return filePath; } catch (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 = []; _selectedLiveCategory = ''; _selectedCountry = ''; _selectedCategory = ''; _isOrganizingCountries = false; _liveStreamsVersion = 0; _invalidateLiveDerivedCaches(); notifyListeners(); } }