From 9f96c574e91fe435c6876eef9c23cdbb717099a7 Mon Sep 17 00:00:00 2001 From: Goldenkrew3000 Date: Thu, 22 Jan 2026 19:22:23 +1000 Subject: [PATCH] Add OpenSubsonic /getInternetRadioStations endpoint /getInternetRadioStations endpoint has been implemented and memory leak tested. It also has been added to the memory testing build. Sorry if there are any bugs, as I have not written memory heavy code in C in a while. --- src/CMakeLists.txt | 3 + src/buildTests.sh | 1 + .../endpoint_getInternetRadioStations.c | 110 ++++++++++++++++++ .../endpoint_getInternetRadioStations.h | 29 +++++ src/libopensubsonic/httpclient.c | 10 ++ src/libopensubsonic/httpclient.h | 1 + src/runTests.c | 17 +++ 7 files changed, 171 insertions(+) create mode 100644 src/libopensubsonic/endpoint_getInternetRadioStations.c create mode 100644 src/libopensubsonic/endpoint_getInternetRadioStations.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4fb8898..c0c20b5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,6 +24,7 @@ add_executable(ossp MACOSX_BUNDLE main.c configHandler.c discordrpc.c + localRadioDBHandler.c gui/gui_entry.cpp player/player.c player/playQueue.cpp @@ -44,6 +45,7 @@ add_executable(ossp MACOSX_BUNDLE libopensubsonic/endpoint_getStarred.c libopensubsonic/endpoint_ping.c libopensubsonic/endpoint_scrobble.c + libopensubsonic/endpoint_getInternetRadioStations.c external/cJSON.c external/cJSON_Utils.c external/libcurl_uriescape.c @@ -55,6 +57,7 @@ add_executable(ossp MACOSX_BUNDLE external/imgui/imgui_demo.cpp external/imgui/backends/imgui_impl_sdl2.cpp external/imgui/backends/imgui_impl_opengl2.cpp + external/sqlite3/sqlite3.c ) set_target_properties(ossp PROPERTIES diff --git a/src/buildTests.sh b/src/buildTests.sh index 4960a63..94bcb69 100755 --- a/src/buildTests.sh +++ b/src/buildTests.sh @@ -17,4 +17,5 @@ gcc runTests.c \ libopensubsonic/endpoint_getAlbumList.c \ libopensubsonic/endpoint_getStarred.c \ libopensubsonic/endpoint_scrobble.c \ + libopensubsonic/endpoint_getInternetRadioStations.c \ -o tests -lcurl diff --git a/src/libopensubsonic/endpoint_getInternetRadioStations.c b/src/libopensubsonic/endpoint_getInternetRadioStations.c new file mode 100644 index 0000000..36a7922 --- /dev/null +++ b/src/libopensubsonic/endpoint_getInternetRadioStations.c @@ -0,0 +1,110 @@ +#include +#include +#include +#include "../external/cJSON.h" +#include "logger.h" +#include "utils.h" +#include "endpoint_getInternetRadioStations.h" + +int opensubsonic_getInternetRadioStations_parse(char* data, opensubsonic_getInternetRadioStations_struct** getInternetRadioStationsStruct) { + // Allocate and initialize struct + *getInternetRadioStationsStruct = malloc(sizeof(opensubsonic_getInternetRadioStations_struct)); + (*getInternetRadioStationsStruct)->status = NULL; + (*getInternetRadioStationsStruct)->errorCode = 0; + (*getInternetRadioStationsStruct)->errorMessage = NULL; + (*getInternetRadioStationsStruct)->radioStationCount = 0; + (*getInternetRadioStationsStruct)->radioStations = NULL; + + // Parse JSON + cJSON* root = cJSON_Parse(data); + if (root == NULL) { + logger_log_error(__func__, "Error parsing JSON."); + return 1; + } + + // Make an object from subsonic-response + cJSON* subsonic_root = cJSON_GetObjectItemCaseSensitive(root, "subsonic-response"); + if (subsonic_root == NULL) { + logger_log_error(__func__, "Error handling JSON - subsonic-response does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Psoj(&(*getInternetRadioStationsStruct)->status, subsonic_root, "status"); + + // Check if API has returned an error + if (strstr((*getInternetRadioStationsStruct)->status, "ok") == NULL) { + // API has not returned 'ok' in status, fetch error, and return + // Check if an error is present + cJSON* subsonic_error = cJSON_GetObjectItemCaseSensitive(subsonic_root, "error"); + if (subsonic_error == NULL) { + // Error not defined in JSON + logger_log_error(__func__, "API has indicated failure through status, but error does not exist."); + cJSON_Delete(root); + return 1; + } + + OSS_Pioj(&(*getInternetRadioStationsStruct)->errorCode, subsonic_error, "code"); + OSS_Psoj(&(*getInternetRadioStationsStruct)->errorMessage, subsonic_error, "message"); + + logger_log_error(__func__, "Error noted in JSON - Code %d: %s", (*getInternetRadioStationsStruct)->errorCode, (*getInternetRadioStationsStruct)->errorMessage); + cJSON_Delete(root); + return 1; + } + + // Make an object from 'internetRadioStations' + cJSON* internetRadioStations_root = cJSON_GetObjectItemCaseSensitive(subsonic_root, "internetRadioStations"); + if (internetRadioStations_root == NULL) { + logger_log_error(__func__, "Error handling JSON - internetRadioStations does not exist."); + cJSON_Delete(root); + return 1; + } + + // Make an object from 'internetRadioStation + cJSON* internetRadioStation_root = cJSON_GetObjectItemCaseSensitive(internetRadioStations_root, "internetRadioStation"); + if (internetRadioStation_root == NULL) { + logger_log_error(__func__, "Error handling JSON - internetRadioStation does not exist."); + cJSON_Delete(root); + return 1; + } + + // Get the amount of radio stiations, then allocate and initialize structs + (*getInternetRadioStationsStruct)->radioStationCount = cJSON_GetArraySize(internetRadioStation_root); + if ((*getInternetRadioStationsStruct)->radioStationCount != 0) { + // If there are no radio stations, don't allocate anything, but let the program run through this. + // Cleanup then happens at the same point, but the following allocation and fetching steps wont happen + // Basically keeps the code cleaner for this edge case (Although this is probably present in other parts **TODO**) + (*getInternetRadioStationsStruct)->radioStations = malloc((*getInternetRadioStationsStruct)->radioStationCount * sizeof(opensubsonic_getInternetRadioStations_radioStations_struct)); + } + + for (int i = 0; i < (*getInternetRadioStationsStruct)->radioStationCount; i++) { + (*getInternetRadioStationsStruct)->radioStations[i].id = NULL; + (*getInternetRadioStationsStruct)->radioStations[i].name = NULL; + (*getInternetRadioStationsStruct)->radioStations[i].streamUrl = NULL; + } + + for (int i = 0; i < (*getInternetRadioStationsStruct)->radioStationCount; i++) { + cJSON* curr_idx_root = cJSON_GetArrayItem(internetRadioStation_root, i); + if (curr_idx_root != NULL) { + OSS_Psoj(&(*getInternetRadioStationsStruct)->radioStations[i].id, curr_idx_root, "id"); + OSS_Psoj(&(*getInternetRadioStationsStruct)->radioStations[i].name, curr_idx_root, "name"); + OSS_Psoj(&(*getInternetRadioStationsStruct)->radioStations[i].streamUrl, curr_idx_root, "streamUrl"); + } + } + + cJSON_Delete(root); + return 0; +} + +void opensubsonic_getInternetRadioStations_struct_free(opensubsonic_getInternetRadioStations_struct** getInternetRadioStationsStruct) { + logger_log_general(__func__, "Freeing /getInternetRadioStations endpoint heap objects."); + if ((*getInternetRadioStationsStruct)->status != NULL) { free((*getInternetRadioStationsStruct)->status); } + if ((*getInternetRadioStationsStruct)->errorMessage != NULL) { free((*getInternetRadioStationsStruct)->errorMessage); } + for (int i = 0; i < (*getInternetRadioStationsStruct)->radioStationCount; i++) { + if ((*getInternetRadioStationsStruct)->radioStations[i].id != NULL) { free((*getInternetRadioStationsStruct)->radioStations[i].id); } + if ((*getInternetRadioStationsStruct)->radioStations[i].name != NULL) { free((*getInternetRadioStationsStruct)->radioStations[i].name); } + if ((*getInternetRadioStationsStruct)->radioStations[i].streamUrl != NULL) { free((*getInternetRadioStationsStruct)->radioStations[i].streamUrl); } + } + if ((*getInternetRadioStationsStruct)->radioStations != NULL) { free((*getInternetRadioStationsStruct)->radioStations); } + if (*getInternetRadioStationsStruct != NULL) { free(*getInternetRadioStationsStruct); } +} diff --git a/src/libopensubsonic/endpoint_getInternetRadioStations.h b/src/libopensubsonic/endpoint_getInternetRadioStations.h new file mode 100644 index 0000000..d13de55 --- /dev/null +++ b/src/libopensubsonic/endpoint_getInternetRadioStations.h @@ -0,0 +1,29 @@ +#ifndef _ENDPOINT_GETINTERNETRADIOSTATIONS_H +#define _ENDPOINT_GETINTERNETRADIOSTATIONS_H + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +typedef struct { + char* id; + char* name; + char* streamUrl; +} opensubsonic_getInternetRadioStations_radioStations_struct; + +typedef struct { + char* status; + int errorCode; + char* errorMessage; + int radioStationCount; + opensubsonic_getInternetRadioStations_radioStations_struct* radioStations; +} opensubsonic_getInternetRadioStations_struct; + +int opensubsonic_getInternetRadioStations_parse(char* data, opensubsonic_getInternetRadioStations_struct** getInternetRadioStationsStruct); +void opensubsonic_getInternetRadioStations_struct_free(opensubsonic_getInternetRadioStations_struct** getInternetRadioStationsStruct); + +#ifdef __cplusplus +} +#endif // __cplusplus + +#endif // _ENDPOINT_GETINTERNETRADIOSTATIONS_H diff --git a/src/libopensubsonic/httpclient.c b/src/libopensubsonic/httpclient.c index 68f4c3b..5fab7ab 100644 --- a/src/libopensubsonic/httpclient.c +++ b/src/libopensubsonic/httpclient.c @@ -19,6 +19,7 @@ #include "endpoint_getAlbumList.h" #include "endpoint_getAlbum.h" #include "endpoint_scrobble.h" +#include "endpoint_getInternetRadioStations.h" static int rc = 0; extern configHandler_config_t* configObj; @@ -214,6 +215,12 @@ void opensubsonic_httpClient_formUrl(opensubsonic_httpClient_URL_t** urlObj) { (*urlObj)->id, submitString); free(submitString); break; + case OPENSUBSONIC_ENDPOINT_GETINTERNETRADIOSTATIONS: + rc = asprintf(&url, "%s://%s/rest/getInternetRadioStations?u=%s&t=%s&s=%s&f=json&v=%s&c=%s", + configObj->opensubsonic_protocol, configObj->opensubsonic_server, configObj->opensubsonic_username, + configObj->internal_opensubsonic_loginToken, configObj->internal_opensubsonic_loginSalt, + configObj->internal_opensubsonic_version, configObj->internal_opensubsonic_clientName); + break; default: logger_log_error(__func__, "Unknown endpoint requested."); break; @@ -270,6 +277,9 @@ void opensubsonic_httpClient_fetchResponse(opensubsonic_httpClient_URL_t** urlOb } else if ((*urlObj)->endpoint == OPENSUBSONIC_ENDPOINT_SCROBBLE) { opensubsonic_scrobble_struct** scrobbleStruct = (opensubsonic_scrobble_struct**)responseObj; opensubsonic_scrobble_parse(httpReq->responseMsg, scrobbleStruct); + } else if ((*urlObj)->endpoint == OPENSUBSONIC_ENDPOINT_GETINTERNETRADIOSTATIONS) { + opensubsonic_getInternetRadioStations_struct** getInternetRadioStationsStruct = (opensubsonic_getInternetRadioStations_struct**)responseObj; + opensubsonic_getInternetRadioStations_parse(httpReq->responseMsg, getInternetRadioStationsStruct); } else { logger_log_error(__func__, "Unknown endpoint requested."); } diff --git a/src/libopensubsonic/httpclient.h b/src/libopensubsonic/httpclient.h index 72296ae..55aa6ce 100644 --- a/src/libopensubsonic/httpclient.h +++ b/src/libopensubsonic/httpclient.h @@ -26,6 +26,7 @@ extern "C" { #define OPENSUBSONIC_ENDPOINT_GETLYRICSBYSONGID 311 #define OPENSUBSONIC_ENDPOINT_GETALBUMLIST 312 #define OPENSUBSONIC_ENDPOINT_SCROBBLE 313 +#define OPENSUBSONIC_ENDPOINT_GETINTERNETRADIOSTATIONS 314 #define OPENSUBSONIC_ENDPOINT_GETALBUMLIST_RANDOM 501 #define OPENSUBSONIC_ENDPOINT_GETALBUMLIST_NEWEST 502 #define OPENSUBSONIC_ENDPOINT_GETALBUMLIST_HIGHEST 503 diff --git a/src/runTests.c b/src/runTests.c index a9253ab..cbf94bb 100644 --- a/src/runTests.c +++ b/src/runTests.c @@ -19,6 +19,7 @@ #include "libopensubsonic/endpoint_getAlbumList.h" #include "libopensubsonic/endpoint_getAlbum.h" #include "libopensubsonic/endpoint_getLyricsBySongId.h" +#include "libopensubsonic/endpoint_getInternetRadioStations.h" configHandler_config_t* configObj = NULL; @@ -179,6 +180,21 @@ void test_libopensubsonic_endpoint_getLyricsBySongId(void) { opensubsonic_httpClient_URL_cleanup(&url); } +void test_libopensubsonic_endpoint_getInternetRadioStations(void) { + logger_log_general(__func__, "Testing getInternetRadioStations endpoint."); + + opensubsonic_httpClient_URL_t* url = malloc(sizeof(opensubsonic_httpClient_URL_t)); + opensubsonic_httpClient_URL_prepare(&url); + url->endpoint = OPENSUBSONIC_ENDPOINT_GETINTERNETRADIOSTATIONS; + opensubsonic_httpClient_formUrl(&url); + + opensubsonic_getInternetRadioStations_struct* getInternetRadioStationsStruct; + opensubsonic_httpClient_fetchResponse(&url, (void**)&getInternetRadioStationsStruct); + + opensubsonic_getInternetRadioStations_struct_free(&getInternetRadioStationsStruct); + opensubsonic_httpClient_URL_cleanup(&url); +} + int main(void) { int rc = 0; @@ -200,6 +216,7 @@ int main(void) { test_libopensubsonic_endpoint_getAlbumList(); test_libopensubsonic_endpoint_getAlbum(); test_libopensubsonic_endpoint_getLyricsBySongId(); + test_libopensubsonic_endpoint_getInternetRadioStations(); // Free config file configHandler_Free(&configObj);