import 'dart:async'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; import '../models/stream_entry.dart'; class M3uParseException implements Exception { final String message; M3uParseException(this.message); @override String toString() => 'M3uParseException: $message'; } class M3UService { final http.Client _client; final Duration cacheTTL; String? _cachedUrl; DateTime? _cachedAt; List? _cachedEntries; M3UService({http.Client? client, this.cacheTTL = const Duration(minutes: 5)}) : _client = client ?? http.Client(); /// Fetches an M3U (over HTTP) and returns parsed list of [StreamEntry]. /// Uses a short in-memory cache to avoid repeated network calls. Future> fetch(String url, {bool forceRefresh = false}) async { if (!forceRefresh && _cachedUrl == url && _cachedEntries != null && _cachedAt != null && DateTime.now().difference(_cachedAt!) < cacheTTL) { return _cachedEntries!; } final resp = await _client.get(Uri.parse(url)); if (resp.statusCode != 200) { throw Exception('Failed to fetch M3U: HTTP ${resp.statusCode}'); } final content = resp.body; final entries = parse(content); // Update cache _cachedUrl = url; _cachedAt = DateTime.now(); _cachedEntries = entries; return entries; } /// Parse raw M3U content into [StreamEntry] list. List parse(String content) { final lines = content.replaceAll('\r', '').split('\n'); final entries = []; String? pendingTitle; for (var raw in lines) { final line = raw.trim(); if (line.isEmpty) continue; if (line.startsWith('#EXTINF')) { // Format: #EXTINF:-1 tvg-id="" tvg-name="Channel" tvg-logo="" group-title="...") ,Display title final parts = line.split(','); if (parts.length >= 2) { pendingTitle = parts.sublist(1).join(',').trim(); } else { // Fallback: try to extract text after the last space final idx = line.indexOf(','); if (idx >= 0 && idx < line.length - 1) pendingTitle = line.substring(idx + 1).trim(); } continue; } if (line.startsWith('#')) continue; // At this point 'line' is a URL final url = line; final title = pendingTitle ?? url; entries.add(StreamEntry(title: title, url: url)); pendingTitle = null; } if (entries.isEmpty) { throw M3uParseException('No entries found in M3U'); } return entries; } }