import 'dart:convert'; import 'package:http/http.dart' as http; import '../modelos/emisora.dart'; /// Cliente para la Radio Browser API (https://api.radio-browser.info/). /// /// Aplica reintentos acotados con rotación de host para tolerar fallos /// transitorios al iniciar. class ServicioRadio { static const _timeoutPorDefecto = Duration(seconds: 10); static const _maxIntentosPorDefecto = 3; static const _retryDelayPorDefecto = Duration(milliseconds: 250); // 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', ]; ServicioRadio({ http.Client? cliente, List? servidores, int maxIntentos = _maxIntentosPorDefecto, Duration retryDelay = _retryDelayPorDefecto, Duration timeout = _timeoutPorDefecto, }) : _cliente = cliente ?? http.Client(), _servidores = (servidores == null || servidores.isEmpty) ? List.from(_servidoresFallback) : List.from(servidores), _maxIntentos = maxIntentos < 1 ? 1 : maxIntentos, _retryDelay = retryDelay, _timeout = timeout; final http.Client _cliente; final List _servidores; final int _maxIntentos; final Duration _retryDelay; final Duration _timeout; String? _servidorActual; int _indiceServidorInicial() { if (_servidorActual == null) { return 0; } final index = _servidores.indexOf(_servidorActual!); return index >= 0 ? index : 0; } String _servidorPorIntento(int indiceBase, int intento) { final index = (indiceBase + intento) % _servidores.length; return _servidores[index]; } Uri _uri(String servidor, String path, Map params) { return Uri.https(servidor, path, { 'hidebroken': 'true', ...params, }); } Future> _get(String path, Map params) async { Exception? ultimoError; final indiceBase = _indiceServidorInicial(); final totalIntentos = _maxIntentos; for (int intento = 0; intento < totalIntentos; intento++) { final servidor = _servidorPorIntento(indiceBase, intento); final uri = _uri(servidor, path, { 'lastcheckok': '1', ...params, }); try { final resp = await _cliente.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; _servidorActual = servidor; return lista .cast>() .map(Emisora.fromApi) .where((e) => e.uuid.isNotEmpty && e.url.isNotEmpty) .toList(); } on Exception catch (e) { ultimoError = e; _servidorActual = null; final ultimoIntento = intento == (totalIntentos - 1); if (!ultimoIntento && _retryDelay > Duration.zero) { await Future.delayed(_retryDelay); } } } throw ultimoError ?? Exception('Error desconocido al consultar la API'); } /// 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 (best effort). Future registrarClick(String uuid) async { try { final servidor = _servidorActual ?? _servidorPorIntento(_indiceServidorInicial(), 0); await _cliente.get( Uri.https(servidor, '/json/url/$uuid'), headers: {'User-Agent': 'PluriWave/0.1.0 (es.freetimelab.pluriwave)'}, ).timeout(_timeout); } catch (_) { // No crítico, ignorar. } } }