Files
iptv-ren/lib/services/iptv_provider.dart

607 lines
20 KiB
Dart

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<XtreamCategory> _liveCategories = [];
List<XtreamCategory> _vodCategories = [];
List<XtreamCategory> _seriesCategories = [];
List<XtreamStream> _liveStreams = [];
List<XtreamStream> _vodStreams = [];
List<XtreamSeries> _seriesList = [];
List<XtreamEpisode> _seriesEpisodes = [];
String _selectedLiveCategory = '';
String _selectedVodCategory = '';
String _selectedCountry = '';
String _selectedCategory = ''; // For special categories like "Fútbol Argentino"
List<String> _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<XtreamCategory> get liveCategories => _liveCategories;
List<XtreamCategory> get vodCategories => _vodCategories;
List<XtreamCategory> get seriesCategories => _seriesCategories;
List<XtreamStream> get liveStreams => _liveStreams;
List<XtreamStream> get vodStreams => _vodStreams;
List<XtreamSeries> get seriesList => _seriesList;
List<XtreamEpisode> get seriesEpisodes => _seriesEpisodes;
String get selectedLiveCategory => _selectedLiveCategory;
String get selectedCountry => _selectedCountry;
String get selectedCategory => _selectedCategory;
List<String> 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<Map<String, dynamic>> get sidebarItems {
final items = <Map<String, dynamic>>[];
// 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<void> 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<void> _loadCategories() async {
try {
_liveCategories = await _api.getLiveCategories();
_vodCategories = await _api.getVodCategories();
_seriesCategories = await _api.getSeriesCategories();
} catch (e) {
_error = e.toString();
}
}
Future<void> 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<String> _extractCountriesFromCategories() {
final countries = <String>{};
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<String, String> _buildCategoryToCountryMap() {
final map = <String, String>{};
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<XtreamStream> 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<void> loadVodStreams([String categoryId = '']) async {
_isLoading = true;
notifyListeners();
try {
_vodStreams = await _api.getVodStreams(categoryId);
_selectedVodCategory = categoryId;
} catch (e) {
_error = e.toString();
}
_isLoading = false;
notifyListeners();
}
Future<void> loadSeries() async {
_isLoading = true;
notifyListeners();
try {
_seriesList = await _api.getSeries();
} catch (e) {
_error = e.toString();
}
_isLoading = false;
notifyListeners();
}
Future<void> 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<void> 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<String> 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<String, int> _groupChannelsByCountry(List<M3UChannel> channels) {
final groups = <String, int>{};
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<String> 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 = <String, List<XtreamStream>>{};
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<void> _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<bool> 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<void> 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();
}
}