This is an automated email from the git hooks/post-receive script.

git pushed a commit to branch main
in repository eradio.

View the commit online.

commit eb2a6737719126f6d23c40f5f0cbfa39554c1fbb
Author: politebot <[email protected]>
AuthorDate: Fri Oct 10 19:41:37 2025 -0500

    Multiple servers
---
 src/appdata.h      |   3 +
 src/http.c         | 272 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 src/http.h         |   1 +
 src/main.c         |   1 +
 src/station_list.c |  15 +--
 src/ui.c           |  34 +++++++
 src/ui.h           |   1 +
 7 files changed, 306 insertions(+), 21 deletions(-)

diff --git a/src/appdata.h b/src/appdata.h
index 29ad24e..37d2219 100644
--- a/src/appdata.h
+++ b/src/appdata.h
@@ -25,11 +25,14 @@ typedef struct _AppData
    Evas_Object *emotion;
    Evas_Object *search_entry;
    Evas_Object *search_hoversel;
+   Evas_Object *server_hoversel;
    Evas_Object *play_pause_btn;
    Evas_Object *stop_btn;
    Evas_Object *search_btn;
    Evas_Object *search_bar;
    Eina_List *stations;
+   Eina_List *api_servers;    // list of strings (hostnames)
+   const char *api_selected;  // currently selected server hostname
    Eina_Bool playing;
    Eina_Hash *favorites;
    Eina_List *favorites_stations;
diff --git a/src/http.c b/src/http.c
index 33d3bc2..dbea4f7 100644
--- a/src/http.c
+++ b/src/http.c
@@ -1,6 +1,11 @@
 #include <Ecore_Con.h>
 #include <libxml/parser.h>
 #include <libxml/xpath.h>
+#include <netdb.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+#include <time.h>
+#include <string.h>
 
 #include "http.h"
 #include "station_list.h"
@@ -9,7 +14,8 @@
 typedef enum _Download_Type
 {
    DOWNLOAD_TYPE_STATIONS,
-   DOWNLOAD_TYPE_ICON
+   DOWNLOAD_TYPE_ICON,
+   DOWNLOAD_TYPE_COUNTER
 } Download_Type;
 
 typedef struct _Download_Context
@@ -22,6 +28,10 @@ typedef struct _Station_Download_Context
 {
    Download_Context base;
    xmlParserCtxtPtr ctxt;
+   Eina_List *servers;     // list of const char* hostnames
+   Eina_List *current;     // current server node
+   char search_type[64];
+   char search_term[512];
 } Station_Download_Context;
 
 typedef struct _Icon_Download_Context
@@ -31,15 +41,37 @@ typedef struct _Icon_Download_Context
    Eina_Binbuf *image_data;
 } Icon_Download_Context;
 
+typedef struct _Counter_Download_Context
+{
+   Download_Context base;
+   Eina_List *servers;     // list of const char* hostnames
+   Eina_List *current;     // current server node
+   char stationuuid[128];
+} Counter_Download_Context;
+
 static Eina_Bool _url_data_cb(void *data, int type, void *event_info);
 static Eina_Bool _url_complete_cb(void *data, int type, void *event_info);
 
+static void _refresh_api_servers(AppData *ad);
+static void _randomize_servers(AppData *ad);
+static const char *_primary_server(AppData *ad);
+static void _populate_station_request(Station_Download_Context *d_ctx, AppData *ad, const char *search_type, const char *search_term);
+static void _issue_station_request(Ecore_Con_Url **url_out, Station_Download_Context *d_ctx);
+static void _retry_next_server_station(Ecore_Con_Url *old_url, Station_Download_Context *d_ctx);
+static void _populate_counter_request(Counter_Download_Context *c_ctx, AppData *ad, const char *uuid);
+static void _issue_counter_request(Ecore_Con_Url **url_out, Counter_Download_Context *c_ctx);
+static void _retry_next_server_counter(Ecore_Con_Url *old_url, Counter_Download_Context *c_ctx);
+
 void
 http_init(AppData *ad)
 {
    ecore_con_init();
    ecore_event_handler_add(ECORE_CON_EVENT_URL_DATA, _url_data_cb, ad);
    ecore_event_handler_add(ECORE_CON_EVENT_URL_COMPLETE, _url_complete_cb, ad);
+
+   _refresh_api_servers(ad);
+   _randomize_servers(ad);
+   ad->api_selected = _primary_server(ad);
 }
 
 void
@@ -51,7 +83,6 @@ http_shutdown(void)
 void
 http_search_stations(AppData *ad, const char *search_term, const char *search_type)
 {
-   char url_str[1024];
    Ecore_Con_Url *url;
    Station_Download_Context *d_ctx;
 
@@ -60,16 +91,30 @@ http_search_stations(AppData *ad, const char *search_term, const char *search_ty
    d_ctx = calloc(1, sizeof(Station_Download_Context));
    d_ctx->base.type = DOWNLOAD_TYPE_STATIONS;
    d_ctx->base.ad = ad;
-
-   snprintf(url_str, sizeof(url_str), "http://de2.api.radio-browser.info/xml/stations/search?%s=%s",
-            search_type, search_term);
-
-   url = ""
+   _populate_station_request(d_ctx, ad, search_type, search_term);
+   _issue_station_request(&url, d_ctx);
    ecore_con_url_additional_header_add(url, "User-Agent", "eradio/1.0");
    ecore_con_url_data_set(url, d_ctx);
    ecore_con_url_get(url);
 }
 
+void
+http_station_click_counter(AppData *ad, const char *uuid)
+{
+   if (!uuid || !uuid[0]) return;
+
+   Counter_Download_Context *c_ctx = calloc(1, sizeof(Counter_Download_Context));
+   c_ctx->base.type = DOWNLOAD_TYPE_COUNTER;
+   c_ctx->base.ad = ad;
+   _populate_counter_request(c_ctx, ad, uuid);
+
+   Ecore_Con_Url *url;
+   _issue_counter_request(&url, c_ctx);
+   ecore_con_url_additional_header_add(url, "User-Agent", "eradio/1.0");
+   ecore_con_url_data_set(url, c_ctx);
+   ecore_con_url_get(url);
+}
+
 void
 http_download_icon(AppData *ad, Elm_Object_Item *list_item, const char *url_str)
 {
@@ -162,9 +207,17 @@ _handle_station_list_complete(Ecore_Con_Event_Url_Complete *ev)
 
     ad = d_ctx->base.ad;
 
+    if (ev->status != 200)
+      {
+         printf("HTTP error %d on %s, trying fallback...\n", ev->status, ecore_con_url_url_get(ev->url_con));
+         _retry_next_server_station(ev->url_con, d_ctx);
+         return;
+      }
+
     if (!d_ctx->ctxt)
       {
-         free(d_ctx);
+         printf("Error: no parser context; retrying next server...\n");
+         _retry_next_server_station(ev->url_con, d_ctx);
          return;
       }
 
@@ -175,7 +228,8 @@ _handle_station_list_complete(Ecore_Con_Event_Url_Complete *ev)
 
     if (doc == NULL)
     {
-        printf("Error: could not parse XML\n");
+        printf("Error: could not parse XML; trying fallback...\n");
+        _retry_next_server_station(ev->url_con, ecore_con_url_data_get(ev->url_con));
         return;
     }
 
@@ -276,7 +330,207 @@ _url_complete_cb(void *data, int type, void *event_info)
       _handle_station_list_complete(ev);
     else if (ctx->type == DOWNLOAD_TYPE_ICON)
       _handle_icon_complete(ev);
+    else if (ctx->type == DOWNLOAD_TYPE_COUNTER)
+      {
+         if (ev->status != 200)
+           {
+              printf("HTTP error %d on %s, trying fallback counter...\n", ev->status, ecore_con_url_url_get(ev->url_con));
+              _retry_next_server_counter(ev->url_con, (Counter_Download_Context *)ctx);
+              return ECORE_CALLBACK_PASS_ON;
+           }
+         // No further action needed; just free context
+         free(ctx);
+      }
 
     ecore_con_url_free(ev->url_con);
     return ECORE_CALLBACK_PASS_ON;
 }
+
+// -------- Helper functions for API server discovery & selection ---------
+
+static void _add_unique_server(AppData *ad, const char *hostname)
+{
+   if (!hostname || !hostname[0]) return;
+   // ensure uniqueness
+   Eina_List *l;
+   const char *h;
+   EINA_LIST_FOREACH(ad->api_servers, l, h)
+   {
+      if (strcmp(h, hostname) == 0) return;
+   }
+   ad->api_servers = eina_list_append(ad->api_servers, eina_stringshare_add(hostname));
+}
+
+static void _refresh_api_servers(AppData *ad)
+{
+   // Resolve all.api.radio-browser.info to get all server IPs
+   struct addrinfo hints = {0}, *res = NULL, *p;
+   hints.ai_family = AF_UNSPEC;
+   hints.ai_socktype = SOCK_STREAM;
+   int ret = getaddrinfo("all.api.radio-browser.info", NULL, &hints, &res);
+   if (ret != 0)
+   {
+      printf("DNS lookup failed: %s\n", gai_strerror(ret));
+      return;
+   }
+
+   for (p = res; p != NULL; p = p->ai_next)
+   {
+      char host[NI_MAXHOST];
+      // Reverse DNS to get human-readable server names
+      int r = getnameinfo(p->ai_addr, p->ai_addrlen, host, sizeof(host), NULL, 0, NI_NAMEREQD);
+      if (r == 0)
+      {
+         _add_unique_server(ad, host);
+      }
+      else
+      {
+         // Fallback: if reverse lookup fails, use the numeric address
+         char addrstr[INET6_ADDRSTRLEN] = {0};
+         void *addr = NULL;
+         if (p->ai_family == AF_INET)
+           addr = &((struct sockaddr_in *)p->ai_addr)->sin_addr;
+         else if (p->ai_family == AF_INET6)
+           addr = &((struct sockaddr_in6 *)p->ai_addr)->sin6_addr;
+         if (addr)
+         {
+            inet_ntop(p->ai_family, addr, addrstr, sizeof(addrstr));
+            _add_unique_server(ad, addrstr);
+         }
+      }
+   }
+
+   freeaddrinfo(res);
+}
+
+static void _randomize_servers(AppData *ad)
+{
+   // Fisher-Yates shuffle on an array copy of the list
+   int n = eina_list_count(ad->api_servers);
+   if (n <= 1) return;
+   char **arr = calloc(n, sizeof(char *));
+   Eina_List *l; const char *h; int i = 0;
+   EINA_LIST_FOREACH(ad->api_servers, l, h) arr[i++] = (char *)h;
+   srand((unsigned int)time(NULL));
+   for (int j = n - 1; j > 0; j--)
+   {
+      int k = rand() % (j + 1);
+      char *tmp = arr[j]; arr[j] = arr[k]; arr[k] = tmp;
+   }
+   // rebuild list in randomized order
+   eina_list_free(ad->api_servers);
+   ad->api_servers = NULL;
+   for (int j = 0; j < n; j++) ad->api_servers = eina_list_append(ad->api_servers, arr[j]);
+   free(arr);
+}
+
+static const char *_primary_server(AppData *ad)
+{
+   if (!ad->api_servers) return NULL;
+   return eina_list_data_get(ad->api_servers);
+}
+
+static void _prepend_selected_as_primary(Eina_List **list, const char *selected)
+{
+   if (!selected || !list) return;
+   Eina_List *l; const char *h; Eina_List *node = NULL;
+   EINA_LIST_FOREACH(*list, l, h)
+   {
+      if ((h == selected) || (h && selected && strcmp(h, selected) == 0))
+      {
+         node = l;
+         break;
+      }
+   }
+   if (node)
+   {
+      const char *data = ""
+      *list = eina_list_remove_list(*list, node);
+      *list = eina_list_prepend(*list, data);
+   }
+}
+
+static void _populate_station_request(Station_Download_Context *d_ctx, AppData *ad, const char *search_type, const char *search_term)
+{
+   strncpy(d_ctx->search_type, search_type ? search_type : "name", sizeof(d_ctx->search_type) - 1);
+   strncpy(d_ctx->search_term, search_term ? search_term : "", sizeof(d_ctx->search_term) - 1);
+   d_ctx->servers = eina_list_clone(ad->api_servers);
+   _prepend_selected_as_primary(&d_ctx->servers, ad->api_selected);
+   d_ctx->current = d_ctx->servers; // start at primary
+}
+
+static void _issue_station_request(Ecore_Con_Url **url_out, Station_Download_Context *d_ctx)
+{
+   const char *server = d_ctx->current ? (const char *)d_ctx->current->data : NULL;
+   char url_str[1024];
+   if (server)
+      snprintf(url_str, sizeof(url_str), "http://%s/xml/stations/search?%s=%s", server, d_ctx->search_type, d_ctx->search_term);
+   else
+      snprintf(url_str, sizeof(url_str), "http://de2.api.radio-browser.info/xml/stations/search?%s=%s", d_ctx->search_type, d_ctx->search_term);
+   *url_out = ecore_con_url_new(url_str);
+}
+
+static void _retry_next_server_station(Ecore_Con_Url *old_url, Station_Download_Context *d_ctx)
+{
+   if (d_ctx->current && d_ctx->current->next)
+   {
+      d_ctx->current = d_ctx->current->next;
+      Ecore_Con_Url *new_url;
+      _issue_station_request(&new_url, d_ctx);
+      ecore_con_url_additional_header_add(new_url, "User-Agent", "eradio/1.0");
+      ecore_con_url_data_set(new_url, d_ctx);
+      ecore_con_url_get(new_url);
+      ecore_con_url_free(old_url);
+   }
+   else
+   {
+      printf("All servers exhausted; failing request.\n");
+      ecore_con_url_free(old_url);
+      // free context and notify UI of failure
+      if (d_ctx->ctxt)
+      {
+         xmlFreeParserCtxt(d_ctx->ctxt);
+         d_ctx->ctxt = NULL;
+      }
+      free(d_ctx);
+   }
+}
+
+static void _populate_counter_request(Counter_Download_Context *c_ctx, AppData *ad, const char *uuid)
+{
+   strncpy(c_ctx->stationuuid, uuid, sizeof(c_ctx->stationuuid) - 1);
+   c_ctx->servers = eina_list_clone(ad->api_servers);
+   _prepend_selected_as_primary(&c_ctx->servers, ad->api_selected);
+   c_ctx->current = c_ctx->servers;
+}
+
+static void _issue_counter_request(Ecore_Con_Url **url_out, Counter_Download_Context *c_ctx)
+{
+   const char *server = c_ctx->current ? (const char *)c_ctx->current->data : NULL;
+   char url_str[1024];
+   if (server)
+      snprintf(url_str, sizeof(url_str), "http://%s/xml/url/%s", server, c_ctx->stationuuid);
+   else
+      snprintf(url_str, sizeof(url_str), "http://de2.api.radio-browser.info/xml/url/%s", c_ctx->stationuuid);
+   *url_out = ecore_con_url_new(url_str);
+}
+
+static void _retry_next_server_counter(Ecore_Con_Url *old_url, Counter_Download_Context *c_ctx)
+{
+   if (c_ctx->current && c_ctx->current->next)
+   {
+      c_ctx->current = c_ctx->current->next;
+      Ecore_Con_Url *new_url;
+      _issue_counter_request(&new_url, c_ctx);
+      ecore_con_url_additional_header_add(new_url, "User-Agent", "eradio/1.0");
+      ecore_con_url_data_set(new_url, c_ctx);
+      ecore_con_url_get(new_url);
+      ecore_con_url_free(old_url);
+   }
+   else
+   {
+      printf("All servers exhausted for counter; giving up.\n");
+      ecore_con_url_free(old_url);
+      free(c_ctx);
+   }
+}
diff --git a/src/http.h b/src/http.h
index ed3d3a4..24e27d8 100644
--- a/src/http.h
+++ b/src/http.h
@@ -8,3 +8,4 @@ void http_search_stations(AppData *ad, const char *search_term, const char *sear
 void http_download_icon(AppData *ad, Elm_Object_Item *list_item, const char *url);
 void _search_btn_clicked_cb(void *data, Evas_Object *obj, void *event_info);
 void _search_entry_activated_cb(void *data, Evas_Object *obj, void *event_info);
+void http_station_click_counter(AppData *ad, const char *uuid);
diff --git a/src/main.c b/src/main.c
index 5545215..09de68f 100644
--- a/src/main.c
+++ b/src/main.c
@@ -15,6 +15,7 @@ elm_main(int argc, char **argv)
    favorites_init(&ad);
    favorites_load(&ad);
    http_init(&ad);
+   ui_update_server_list(&ad);
    radio_player_init(&ad);
 
    elm_run();
diff --git a/src/station_list.c b/src/station_list.c
index 0de62dc..77de71b 100644
--- a/src/station_list.c
+++ b/src/station_list.c
@@ -7,19 +7,10 @@ static void _favorite_btn_clicked_cb(void *data, Evas_Object *obj, void *event_i
 static void _favorite_remove_btn_clicked_cb(void *data, Evas_Object *obj, void *event_info);
 
 static void
-_station_click_counter_request(Station *st)
+_station_click_counter_request(AppData *ad, Station *st)
 {
-   char url_str[1024];
-   Ecore_Con_Url *url;
-
    if (!st || !st->stationuuid) return;
-
-   snprintf(url_str, sizeof(url_str), "http://de2.api.radio-browser.info/xml/url/%s",
-            st->stationuuid);
-
-   url = ""
-   ecore_con_url_additional_header_add(url, "User-Agent", "eradio/1.0");
-   ecore_con_url_get(url);
+   http_station_click_counter(ad, st->stationuuid);
 }
 
 void
@@ -31,7 +22,7 @@ _list_item_selected_cb(void *data, Evas_Object *obj, void *event_info)
 
    if (!st) return;
 
-   _station_click_counter_request(st);
+   _station_click_counter_request(ad, st);
    radio_player_play(ad, st->url);
 }
 
diff --git a/src/ui.c b/src/ui.c
index 346a1bc..111f8af 100644
--- a/src/ui.c
+++ b/src/ui.c
@@ -8,6 +8,7 @@ static void _app_exit_cb(void *data, Evas_Object *obj, void *event_info);
 static void _hoversel_item_selected_cb(void *data, Evas_Object *obj, void *event_info);
 static void _tb_favorites_clicked_cb(void *data, Evas_Object *obj, void *event_info);
 static void _tb_search_clicked_cb(void *data, Evas_Object *obj, void *event_info);
+static void _server_item_selected_cb(void *data, Evas_Object *obj, void *event_info);
 
 // Forward declarations for callbacks
 void _play_pause_btn_clicked_cb(void *data, Evas_Object *obj, void *event_info);
@@ -105,6 +106,13 @@ ui_create(AppData *ad)
    elm_box_pack_end(ad->search_bar, ad->search_hoversel);
    evas_object_show(ad->search_hoversel);
 
+   // Server selection hoversel, populated after HTTP init discovers servers
+   ad->server_hoversel = elm_hoversel_add(ad->win);
+   elm_hoversel_hover_parent_set(ad->server_hoversel, ad->win);
+   elm_object_text_set(ad->server_hoversel, "server");
+   elm_box_pack_end(ad->search_bar, ad->server_hoversel);
+   evas_object_show(ad->server_hoversel);
+
    ad->search_btn = elm_button_add(ad->win);
    elm_object_text_set(ad->search_btn, "Search");
    elm_box_pack_end(ad->search_bar, ad->search_btn);
@@ -164,6 +172,32 @@ ui_create(AppData *ad)
    evas_object_show(ad->win);
 }
 
+void ui_update_server_list(AppData *ad)
+{
+   if (!ad || !ad->server_hoversel) return;
+   Eina_List *l; const char *host;
+   EINA_LIST_FOREACH(ad->api_servers, l, host)
+   {
+      elm_hoversel_item_add(ad->server_hoversel, host, NULL, ELM_ICON_NONE, _server_item_selected_cb, ad);
+   }
+   if (ad->api_selected && ad->api_selected[0])
+      elm_object_text_set(ad->server_hoversel, ad->api_selected);
+}
+
+static void _server_item_selected_cb(void *data, Evas_Object *obj, void *event_info)
+{
+   AppData *ad = data;
+   if (!ad) return;
+   Elm_Object_Item *it = event_info;
+   const char *label = elm_object_item_text_get(it);
+   if (label && label[0])
+   {
+      elm_object_text_set(obj, label);
+      if (ad->api_selected) eina_stringshare_del(ad->api_selected);
+      ad->api_selected = eina_stringshare_add(label);
+   }
+}
+
 static void
 _tb_favorites_clicked_cb(void *data, Evas_Object *obj EINA_UNUSED, void *event_info EINA_UNUSED)
 {
diff --git a/src/ui.h b/src/ui.h
index 0c7ef27..84e40d1 100644
--- a/src/ui.h
+++ b/src/ui.h
@@ -5,3 +5,4 @@
 typedef struct _AppData AppData;
 
 void ui_create(AppData *ad);
+void ui_update_server_list(AppData *ad);

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to