/// Modelo de datos de una emisora de radio. /// /// Unifica los campos de la Radio Browser API con los de la tabla SQLite /// de favoritos. Los campos opcionales pueden ser null cuando la emisora /// no dispone de esa información. class Emisora { final int? id; final String uuid; final String nombre; final String url; final String? favicon; final String? pais; final String? codigoPais; // ISO 3166-1 alpha-2 final String? idioma; final String? tags; final String? codec; final int? bitrate; final int votes; final int clickcount; final int orden; const Emisora({ this.id, required this.uuid, required this.nombre, required this.url, this.favicon, this.pais, this.codigoPais, this.idioma, this.tags, this.codec, this.bitrate, this.votes = 0, this.clickcount = 0, this.orden = 0, }); /// Construye una [Emisora] desde la respuesta JSON de Radio Browser API. factory Emisora.fromApi(Map json) { return Emisora( uuid: json['stationuuid'] as String? ?? '', nombre: json['name'] as String? ?? 'Sin nombre', url: json['url_resolved'] as String? ?? json['url'] as String? ?? '', favicon: _nonEmpty(json['favicon'] as String?), pais: _nonEmpty(json['country'] as String?), codigoPais: _nonEmpty(json['countrycode'] as String?), idioma: _nonEmpty(json['language'] as String?), tags: _nonEmpty(json['tags'] as String?), codec: _nonEmpty(json['codec'] as String?), bitrate: json['bitrate'] as int?, votes: json['votes'] as int? ?? 0, clickcount: json['clickcount'] as int? ?? 0, ); } /// Construye una [Emisora] desde una fila de la tabla `favoritos`. factory Emisora.fromMap(Map map) { return Emisora( id: map['id'] as int?, uuid: map['uuid'] as String, nombre: map['nombre'] as String, url: map['url'] as String, favicon: map['favicon'] as String?, pais: map['pais'] as String?, codigoPais: map['codigo_pais'] as String?, idioma: map['idioma'] as String?, tags: map['tags'] as String?, codec: map['codec'] as String?, bitrate: map['bitrate'] as int?, votes: map['votes'] as int? ?? 0, clickcount: map['clickcount'] as int? ?? 0, orden: map['orden'] as int? ?? 0, ); } /// Serializa para inserción/actualización en SQLite. Map toMap() { return { 'uuid': uuid, 'nombre': nombre, 'url': url, 'favicon': favicon, 'pais': pais, 'codigo_pais': codigoPais, 'idioma': idioma, 'tags': tags, 'codec': codec, 'bitrate': bitrate, 'votes': votes, 'clickcount': clickcount, 'orden': orden, }; } Emisora copyWith({ int? id, String? uuid, String? nombre, String? url, String? favicon, String? pais, String? codigoPais, String? idioma, String? tags, String? codec, int? bitrate, int? votes, int? clickcount, int? orden, }) { return Emisora( id: id ?? this.id, uuid: uuid ?? this.uuid, nombre: nombre ?? this.nombre, url: url ?? this.url, favicon: favicon ?? this.favicon, pais: pais ?? this.pais, codigoPais: codigoPais ?? this.codigoPais, idioma: idioma ?? this.idioma, tags: tags ?? this.tags, codec: codec ?? this.codec, bitrate: bitrate ?? this.bitrate, votes: votes ?? this.votes, clickcount: clickcount ?? this.clickcount, orden: orden ?? this.orden, ); } /// Lista de géneros/tags como lista limpia. List get generos { if (tags == null || tags!.isEmpty) return []; return tags!.split(',').map((t) => t.trim()).where((t) => t.isNotEmpty).toList(); } static String? _nonEmpty(String? s) => (s == null || s.trim().isEmpty) ? null : s.trim(); @override String toString() => 'Emisora(uuid: $uuid, nombre: $nombre)'; @override bool operator ==(Object other) => identical(this, other) || other is Emisora && runtimeType == other.runtimeType && uuid == other.uuid; @override int get hashCode => uuid.hashCode; }