import 'dart:convert'; import 'dart:math'; import 'package:http/http.dart' as http; import '../modelos/emisora.dart'; /// Cliente para la Radio Browser API (https://api.radio-browser.info/). /// /// Selecciona automáticamente un servidor disponible de entre los DNS /// resueltos para `all.api.radio-browser.info` y rota en caso de error. /// /// ### Rate limiting /// La API no tiene límite documentado, pero por cortesía limitamos a /// peticiones con `?limit` explícito y no hacemos polling automático. class ServicioRadio { static const _dnsHost = 'all.api.radio-browser.info'; static const _timeout = Duration(seconds: 10); // Servidores conocidos como fallback si el DNS falla static const _servidoresFallback = [ 'de1.api.radio-browser.info', 'nl1.api.radio-browser.info', 'at1.api.radio-browser.info', ]; String? _servidorActual; Future _servidor() async { if (_servidorActual != null) return _servidorActual!; // Intentar DNS lookup simplificado — usamos fallback directamente final servidores = List.from(_servidoresFallback)..shuffle(Random()); _servidorActual = servidores.first; return _servidorActual!; } Uri _uri(String servidor, String path, Map params) { return Uri.https(servidor, path, { 'hidebroken': 'true', ...params, }); } Future> _get(String path, Map params) async { final servidor = await _servidor(); final uri = _uri(servidor, path, params); try { final resp = await http.get(uri, headers: { 'User-Agent': 'PluriWave/0.1.0 (es.freetimelab.pluriwave)', }).timeout(_timeout); if (resp.statusCode != 200) { throw Exception('API error ${resp.statusCode}'); } final lista = json.decode(resp.body) as List; return lista .cast>() .map(Emisora.fromApi) .where((e) => e.uuid.isNotEmpty && e.url.isNotEmpty) .toList(); } catch (e) { // Rotar servidor en el siguiente intento _servidorActual = null; rethrow; } } /// Emisoras más votadas globalmente. Future> obtenerPopulares({int limit = 30}) async { return _get('/json/stations/topvote/$limit', {}); } /// Emisoras más escuchadas (por clicks) globalmente. Future> obtenerTendencias({int limit = 20}) async { return _get('/json/stations/topclick/$limit', {}); } /// Buscar por nombre de emisora. Future> buscarPorNombre(String query, {int limit = 30}) async { return _get('/json/stations/search', { 'name': query, 'limit': limit.toString(), 'order': 'votes', 'reverse': 'true', }); } /// Buscar por código de país (ISO 3166-1 alpha-2, e.g. 'ES', 'US'). Future> buscarPorPais(String codigoPais, {int limit = 50}) async { return _get('/json/stations/bycountrycodeexact/$codigoPais', { 'limit': limit.toString(), 'order': 'votes', 'reverse': 'true', }); } /// Buscar por idioma (e.g. 'spanish', 'english'). Future> buscarPorIdioma(String idioma, {int limit = 30}) async { return _get('/json/stations/bylanguageexact/$idioma', { 'limit': limit.toString(), 'order': 'votes', 'reverse': 'true', }); } /// Buscar por tag/género (e.g. 'rock', 'jazz', 'pop'). Future> buscarPorTag(String tag, {int limit = 30}) async { return _get('/json/stations/bytagexact/$tag', { 'limit': limit.toString(), 'order': 'votes', 'reverse': 'true', }); } /// Búsqueda combinada: permite combinar nombre, país, idioma y tag. Future> buscar({ String? nombre, String? pais, String? idioma, String? tag, int limit = 30, }) async { return _get('/json/stations/search', { if (nombre != null && nombre.isNotEmpty) 'name': nombre, if (pais != null && pais.isNotEmpty) 'countrycode': pais, if (idioma != null && idioma.isNotEmpty) 'language': idioma, if (tag != null && tag.isNotEmpty) 'tag': tag, 'limit': limit.toString(), 'order': 'votes', 'reverse': 'true', }); } /// Registrar un click en la API (buenas prácticas de ciudadanía API). Future registrarClick(String uuid) async { try { final servidor = await _servidor(); await http.get( Uri.https(servidor, '/json/url/$uuid'), headers: {'User-Agent': 'PluriWave/0.1.0 (es.freetimelab.pluriwave)'}, ).timeout(_timeout); } catch (_) { // No crítico — ignorar silenciosamente } } }