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.