[vlc-commits] [Git][videolan/vlc][master] services_discovery: add Online Radio Browser module
Steve Lhomme (@robUx4)
gitlab at videolan.org
Wed Aug 20 11:38:51 UTC 2025
Steve Lhomme pushed to branch master at VideoLAN / VLC
Commits:
c6d6c0c5 by Zyad Ayad at 2025-08-20T11:38:35+00:00
services_discovery: add Online Radio Browser module
- - - - -
2 changed files:
- modules/services_discovery/Makefile.am
- + modules/services_discovery/radio.c
Changes:
=====================================
modules/services_discovery/Makefile.am
=====================================
@@ -4,6 +4,9 @@ sd_LTLIBRARIES =
libpodcast_plugin_la_SOURCES = services_discovery/podcast.c
sd_LTLIBRARIES += libpodcast_plugin.la
+libradio_plugin_la_SOURCES = services_discovery/radio.c
+sd_LTLIBRARIES += libradio_plugin.la
+
libsap_plugin_la_SOURCES = services_discovery/sap.c access/rtp/sdp.c
libsap_plugin_la_CPPFLAGS = $(AM_CPPFLAGS)
libsap_plugin_la_LIBADD = $(SOCKET_LIBS) $(LIBZ)
=====================================
modules/services_discovery/radio.c
=====================================
@@ -0,0 +1,518 @@
+/*****************************************************************************
+ * radio.c: Online Radio Browser services discovery module
+ *****************************************************************************
+ * Copyright (C) 2025 the VideoLAN team
+ *
+ * Authors: Zyad M. Ayad <zyad.moh.1011 at gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <vlc_common.h>
+#include <vlc_plugin.h>
+#include <vlc_services_discovery.h>
+#include <vlc_stream.h>
+#include <vlc_access.h>
+#include <string.h>
+#include <vlc_interrupt.h>
+#include <ctype.h>
+
+#define DEFAULT_BASE_URL "https://all.api.radio-browser.info/" // Default Base URL for the Radio Browser API
+#define MRL_PREFIX "radio://"
+#define CSV_MAX_FIELDS 100
+
+typedef struct
+{
+ vlc_thread_t thread;
+ vlc_interrupt_t *interrupt;
+
+} services_discovery_sys_t;
+
+typedef struct
+{
+ char *base_url;
+} access_sys_t;
+
+typedef struct
+{
+ stream_t *stream;
+ char *header_line;
+ char **header_fields;
+ size_t fields_count;
+ char *line;
+ char **fields;
+} csv_parser;
+
+/*
+ TODO: Handle CSV Fields that has '\n' inside quoted fields
+ Currently this module skips stations with such fields
+ Reason: we are using vlc_stream_ReadLine which reads until '\n'
+*/
+static int parse_csv_line(char *line, char **fields, size_t max_fields)
+{
+ size_t field_count = 0;
+ char *p = line;
+ char *start = NULL;
+
+ enum
+ {
+ STATE_START,
+ STATE_IN_FIELD,
+ STATE_IN_QUOTED_FIELD
+ } state = STATE_START;
+
+ while (p && *p)
+ {
+ if (field_count >= max_fields)
+ return -1; // Exceeded max fields
+
+ switch (state)
+ {
+ case STATE_START:
+ if (*p == '"')
+ {
+ state = STATE_IN_QUOTED_FIELD;
+ start = ++p; // Skip the opening quote
+ }
+ else if (*p == ',')
+ {
+ fields[field_count++] = p;
+ *p++ = '\0';
+ }
+ else
+ {
+ state = STATE_IN_FIELD;
+ start = p;
+ }
+ break;
+
+ case STATE_IN_FIELD:
+ {
+ char *comma = strchr(p, ',');
+ if (comma)
+ {
+ *comma = '\0';
+ fields[field_count++] = start;
+ p = comma + 1;
+ state = STATE_START;
+ }
+ else
+ {
+ fields[field_count++] = start;
+ p = NULL;
+ }
+ break;
+ }
+
+ case STATE_IN_QUOTED_FIELD:
+ {
+ char *quote = strchr(p, '"');
+ if (!quote)
+ return -1;
+
+ if (*(quote + 1) == '"')
+ {
+ // Escaped quote
+ memmove(quote, quote + 1, strlen(quote));
+ p = quote + 1;
+ }
+ else
+ {
+ *quote = '\0';
+ fields[field_count++] = start;
+ p = quote + 1;
+ state = STATE_START;
+
+ if (*p == ',')
+ {
+ p++; // Move past comma
+ }
+ else if (*p == '\0')
+ {
+ p = NULL; // End of input
+ }
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+
+ return (int)field_count;
+}
+
+static int csv_parser_read_line(csv_parser *parser)
+{
+
+ free(parser->line);
+ parser->line = NULL;
+
+ /* read next line */
+ parser->line = vlc_stream_ReadLine(parser->stream);
+ if (!parser->line)
+ {
+ return 0; // End of stream
+ }
+
+ /* parse the line into fields */
+ int field_count = parse_csv_line(parser->line, parser->fields, parser->fields_count);
+ if (field_count < 0 || (size_t)field_count != parser->fields_count)
+ {
+ return -1;
+ }
+
+ return 1;
+}
+
+static void csv_parser_free(csv_parser *parser)
+{
+ if (parser->stream)
+ {
+ vlc_stream_Delete(parser->stream);
+ }
+ free(parser->header_line);
+ free(parser->line);
+ free(parser->fields);
+ free(parser->header_fields);
+ free(parser);
+}
+
+static int csv_parser_get_field_index(csv_parser *parser, const char *field_name)
+{
+ if (!parser || !field_name || !parser->header_fields)
+ {
+ return -1; // Invalid parser or field name
+ }
+
+ for (size_t i = 0; i < parser->fields_count; i++)
+ {
+ if (strcmp(parser->header_fields[i], field_name) == 0)
+ {
+ return i; // Return the field value
+ }
+ }
+
+ return -1; // Field not found
+}
+
+static csv_parser *csv_parser_init(void *data, char *psz_url, int max_fields)
+{
+
+ services_discovery_t *p_sd = (services_discovery_t *)data;
+
+ csv_parser *parser = malloc(sizeof(csv_parser));
+ if (!parser)
+ {
+ return NULL;
+ }
+
+ parser->stream = vlc_stream_NewURL(p_sd, psz_url);
+ if (!parser->stream)
+ {
+ free(parser);
+ return NULL;
+ }
+
+ parser->header_line = NULL;
+ parser->fields_count = max_fields;
+ parser->line = NULL;
+ parser->fields = malloc(max_fields * sizeof(char *));
+ if (!parser->fields)
+ {
+ vlc_stream_Delete(parser->stream);
+ free(parser);
+ return NULL;
+ }
+ parser->header_fields = malloc(max_fields * sizeof(char *));
+ if (!parser->header_fields)
+ {
+ free(parser->fields);
+ vlc_stream_Delete(parser->stream);
+ free(parser);
+ return NULL;
+ }
+
+ parser->header_line = vlc_stream_ReadLine(parser->stream);
+ if (!parser->header_line)
+ {
+ free(parser->fields);
+ free(parser->header_fields);
+ vlc_stream_Delete(parser->stream);
+ free(parser);
+ return NULL;
+ }
+
+ int header_fields_count = parse_csv_line(parser->header_line, parser->header_fields, max_fields);
+ if (header_fields_count < 0)
+ {
+ free(parser->fields);
+ free(parser->header_line);
+ free(parser->header_fields);
+ vlc_stream_Delete(parser->stream);
+ free(parser);
+ return NULL;
+ }
+
+ parser->fields_count = header_fields_count;
+
+ return parser;
+}
+
+static void *Run(void *data)
+{
+
+ vlc_thread_set_name("vlc-radio");
+
+ services_discovery_t *p_sd = (services_discovery_t *)data;
+ services_discovery_sys_t *p_sys = p_sd->p_sys;
+
+ char *base_url = var_InheritString(p_sd, "radio-browser-baseurl");
+ if (!base_url)
+ return NULL;
+
+ char *countries_endpoint;
+ if (asprintf(&countries_endpoint, "%scsv/countries", base_url) < 0)
+ {
+ free(base_url);
+ return NULL;
+ }
+ free(base_url);
+
+ vlc_interrupt_set(p_sys->interrupt);
+
+ csv_parser *parser = csv_parser_init(p_sd, countries_endpoint, CSV_MAX_FIELDS);
+ free(countries_endpoint);
+ if (!parser)
+ {
+ msg_Err(p_sd, "Failed to initialize CSV parser for countries");
+ return NULL;
+ }
+ int name_index = csv_parser_get_field_index(parser, "name");
+ int iso_3166_1_index = csv_parser_get_field_index(parser, "iso_3166_1");
+
+ if (name_index < 0 || iso_3166_1_index < 0)
+ {
+ msg_Err(p_sd, "Missing required fields in country data");
+ csv_parser_free(parser);
+ return NULL;
+ }
+
+ int ret;
+ while ((ret = csv_parser_read_line(parser)))
+ {
+ if (vlc_killed())
+ break;
+ if (ret < 0)
+ {
+ continue;
+ }
+
+ if (strlen(parser->fields[name_index]) == 0 || strlen(parser->fields[iso_3166_1_index]) != 2)
+ {
+ continue;
+ }
+
+ char *mrl;
+ if (asprintf(&mrl, MRL_PREFIX "%s", parser->fields[iso_3166_1_index]) < 0)
+ {
+ continue;
+ }
+
+ input_item_t *country_node = input_item_NewDirectory(mrl, parser->fields[name_index], ITEM_NET);
+ free(mrl);
+ if (!country_node)
+ {
+ continue;
+ }
+
+ char flag[256];
+ snprintf(flag, sizeof(flag), "https://flagsapi.com/%s/flat/64.png", parser->fields[iso_3166_1_index]);
+ input_item_SetMeta(country_node, vlc_meta_ArtworkURL, flag);
+
+ services_discovery_AddItem(p_sd, country_node);
+ }
+ csv_parser_free(parser);
+ return NULL;
+}
+
+static int Open(vlc_object_t *p_this)
+{
+ services_discovery_t *p_sd = (services_discovery_t *)p_this;
+ services_discovery_sys_t *p_sys = malloc(sizeof(*p_sys));
+ if (!p_sys)
+ return VLC_ENOMEM;
+
+ p_sd->p_sys = p_sys;
+ p_sys->interrupt = vlc_interrupt_create();
+ if (!p_sys->interrupt)
+ {
+ free(p_sys);
+ return VLC_ENOMEM;
+ }
+
+ p_sd->description = _("Radio");
+
+ if (vlc_clone(&p_sys->thread, Run, p_sd))
+ {
+ vlc_interrupt_destroy(p_sys->interrupt);
+ free(p_sys);
+ return VLC_EGENERIC;
+ }
+ return VLC_SUCCESS;
+}
+
+static void Close(vlc_object_t *p_this)
+{
+ services_discovery_t *p_sd = (services_discovery_t *)p_this;
+ services_discovery_sys_t *p_sys = p_sd->p_sys;
+
+ vlc_interrupt_kill(p_sys->interrupt);
+ vlc_join(p_sys->thread, NULL);
+ vlc_interrupt_destroy(p_sys->interrupt);
+ free(p_sys);
+}
+
+static int ReadDirectory(stream_t *p_access, input_item_node_t *p_node)
+{
+ access_sys_t *p_sys = p_access->p_sys;
+
+ if (!p_sys || !p_sys->base_url)
+ {
+ return VLC_EGENERIC;
+ }
+
+ // Make sure the location is TWO characters, because empty countrycode will cause this moudle to load "ALL" stations
+ if (strlen(p_access->psz_location) != 2)
+ {
+ return VLC_EGENERIC;
+ }
+
+ char *stations_endpoint;
+ if (asprintf(&stations_endpoint, "%scsv/stations/bycountrycodeexact/%s?hidebroken=true&order=random",
+ p_sys->base_url, p_access->psz_location) < 0)
+ {
+ return VLC_ENOMEM;
+ }
+
+ csv_parser *parser = csv_parser_init(p_access, stations_endpoint, CSV_MAX_FIELDS);
+ free(stations_endpoint);
+
+ if (!parser)
+ {
+ return VLC_EGENERIC;
+ }
+
+ int name_index = csv_parser_get_field_index(parser, "name");
+ int url_index = csv_parser_get_field_index(parser, "url");
+ int favicon_index = csv_parser_get_field_index(parser, "favicon");
+ if (name_index < 0 || url_index < 0)
+ {
+ msg_Err(p_access, "Missing required fields in station data");
+ csv_parser_free(parser);
+ return VLC_EGENERIC;
+ }
+
+ int ret;
+ while ((ret = csv_parser_read_line(parser)))
+ {
+ if(vlc_killed())
+ break;
+ if (ret < 0)
+ {
+ continue;
+ }
+
+ if (strlen(parser->fields[name_index]) == 0 || strlen(parser->fields[url_index]) == 0)
+ {
+ continue;
+ }
+
+ input_item_t *station_item = input_item_New(parser->fields[url_index], parser->fields[name_index]);
+ if (!station_item)
+ {
+ continue;
+ }
+
+ if (favicon_index >= 0 && strlen(parser->fields[favicon_index]) > 0)
+ {
+ input_item_SetMeta(station_item, vlc_meta_ArtworkURL, parser->fields[favicon_index]);
+ }
+ input_item_node_AppendItem(p_node, station_item);
+ input_item_Release(station_item);
+ }
+ csv_parser_free(parser);
+ return VLC_SUCCESS;
+}
+
+static int OpenAccess(vlc_object_t *p_this)
+{
+ stream_t *p_access = (stream_t *)p_this;
+ access_sys_t *p_sys = malloc(sizeof(*p_sys));
+ if (!p_sys)
+ return VLC_ENOMEM;
+
+ p_sys->base_url = var_InheritString(p_access, "radio-browser-baseurl");
+ if (!p_sys->base_url)
+ {
+ free(p_sys);
+ return VLC_EGENERIC;
+ }
+
+ p_access->p_sys = p_sys;
+ p_access->pf_readdir = ReadDirectory;
+ p_access->pf_control = access_vaDirectoryControlHelper;
+
+ return VLC_SUCCESS;
+}
+
+static void CloseAccess(vlc_object_t *p_this)
+{
+ stream_t *p_access = (stream_t *)p_this;
+ access_sys_t *p_sys = p_access->p_sys;
+
+ free(p_sys->base_url);
+ free(p_sys);
+}
+
+VLC_SD_PROBE_HELPER("radio", N_("Radio Browser"), SD_CAT_INTERNET)
+
+#define BASE_URL_TEXT N_("Radio Browser Api Base URL")
+#define BASE_URL_LONGTEXT N_("Enter the base URL for the Radio Browser API. The default is " DEFAULT_BASE_URL)
+
+vlc_module_begin()
+ set_shortname("Radio")
+ set_description(N_("Radio Browser services discovery"))
+ set_subcategory(SUBCAT_PLAYLIST_SD)
+ set_capability("services_discovery", 0)
+ set_callbacks(Open, Close)
+ add_shortcut("radio")
+ add_string("radio-browser-baseurl", DEFAULT_BASE_URL, BASE_URL_TEXT,
+ BASE_URL_LONGTEXT)
+
+ add_submodule()
+ set_description(N_("Radio Browser access"))
+ set_subcategory(SUBCAT_INPUT_ACCESS)
+ set_capability("access", 0)
+ set_callbacks(OpenAccess, CloseAccess)
+ add_shortcut("radio")
+
+ VLC_SD_PROBE_SUBMODULE
+ vlc_module_end()
View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/c6d6c0c5966023080a4b98a2091fab01f4713f5c
--
View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/c6d6c0c5966023080a4b98a2091fab01f4713f5c
You're receiving this email because of your account on code.videolan.org.
VideoLAN code repository instance
More information about the vlc-commits
mailing list