/*
Groundwave - an internet radio stream player.
Copyright (C) 2026 Visiblink
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
#include
#include
#include
#include
#include
#include
/****************************************************************************/
/* Global Constants */
const char s[2] = ",";
const int SEL_NUM_MAX_LEN = 4;
const int SN_MAX_LEN = 25;
const int URL_MAX_LEN = 100;
const int CSV_LIN_MAX_LEN = 127;
const int MAX_STATIONS = 48;
/****************************************************************************/
/* Global Variables */
char db_file_location[100];
int db_file_line_count;
/****************************************************************************/
/* Function Declarations */
int Menu();
int CreateDatabase();
int ClearInput(char *station_number_copy);
int ValidateNumericalInput(char *station_number_copy, int station_number_integer);
int DisplayStationList();
int PlayStation (int stream_number);
int AddStation();
int DeleteStation(int station_number_integer);
int EditStation(int station_number_integer);
int MoveStation(int station_number_integer);
/****************************************************************************/
int main()
{
CreateDatabase();
Menu();
return 0;
}
/****************************************************************************/
int CreateDatabase()
{
char sys_command[107];
char directory_path[87];
char *homedir = getenv("HOME");
char appdir[27] = "/.local/share/groundwave/";
/* Create a path to the db directory */
strcpy (directory_path, homedir);
strcat (directory_path, appdir);
/* Create a path to the db csv file */
strcpy (db_file_location, directory_path);
strcat (db_file_location, "groundwaverc");
/* Check that the db file exists */
FILE *fpointer;
fpointer = fopen (db_file_location, "a");
if (fpointer == NULL)
{
/* If there is no db file, check whether the db directory exists.
Create the db directory if necessary */
DIR *groundwave_directory;
groundwave_directory=opendir(directory_path);
if (groundwave_directory==NULL)
{
strcpy(sys_command, "mkdir ");
strcat(sys_command, directory_path);
system(sys_command);
}
/* Create the db file */
strcpy(sys_command, "touch ");
strcat(sys_command, db_file_location);
system(sys_command);
}
if (fpointer!=NULL)
{
fclose(fpointer);
}
return 0;
}
/****************************************************************************/
int Menu()
{
char *p;
char user_input[SEL_NUM_MAX_LEN];
char station_number[SEL_NUM_MAX_LEN];
char station_URL[URL_MAX_LEN];
char play_command[110];
int station_number_integer;
int numerical_input;
int random_max;
system("clear");
printf("\n \033[32m\033[1mGroundwave - Internet Radio Player\033[0m\n\n");
printf(" \033[1mStation List:\033[0m\n\n");
DisplayStationList();
printf("\n \033[1mCommand Menu:\033[0m\n\n");
printf("%-*s %-*s %-*s",
40, " \033[7m a \033[0m add station",
39, "\033[7m e \033[0m edit station",
10, "\033[7m p \033[0m play URL\n");
printf("%-*s %-*s %-*s",
40, " \033[7m d \033[0m delete station",
39, "\033[7m m \033[0m move station",
9, "\033[7m q \033[0m quit\n");
printf("\n \033[1mEnter the number or letter of your selection: ");
/* Accept User Input */
fgets(user_input, sizeof(user_input), stdin);
printf("\033[0m");
ClearInput(user_input);
user_input[strcspn(user_input, "\n")] = 0;
/* Process User Input */
if (user_input[0]=='a' && !user_input[1]) // accept only a single character
{
AddStation();
}
else if (user_input[0]=='d' && !user_input[1])
{
printf("\n Enter the number of the station to delete: ");
fgets(station_number, sizeof(station_number), stdin);
station_number_integer = atoi(station_number);
station_number_integer=station_number_integer-1;
ClearInput(station_number);
ValidateNumericalInput(station_number, station_number_integer);
DeleteStation(station_number_integer);
}
else if (user_input[0]=='e' && !user_input[1])
{
printf("\n Enter the number of the station to edit: ");
fgets(station_number, sizeof(station_number), stdin);
station_number_integer = atoi(station_number);
station_number_integer=station_number_integer-1;
ClearInput(station_number);
ValidateNumericalInput(station_number, station_number_integer);
EditStation(station_number_integer);
}
else if (user_input[0]=='m' && !user_input[1])
{
printf("\n Enter the number of the station to move: ");
fgets(station_number, sizeof(station_number), stdin);
station_number_integer = atoi(station_number);
station_number_integer=station_number_integer-1;
ClearInput(station_number);
ValidateNumericalInput(station_number, station_number_integer);
MoveStation(station_number_integer);
}
else if (user_input[0]=='p' && !user_input[1])
{
printf("\n Enter the station URL: ");
fgets(station_URL, sizeof(station_URL), stdin);
ClearInput(station_URL);
/* Prevent local filesystem access */
if ((station_URL[0] != 'h') ||
(station_URL[1] != 't') ||
(station_URL[2] != 't') ||
(station_URL[3] != 'p'))
{
Menu();
}
/* Replace the newline character with a zero to terminate
the string. */
station_URL[strcspn(station_URL, "\n")] = 0;
strcpy(play_command, "gst123 -q ");
strcat(play_command,station_URL);
printf("\033[?25l");
printf("\n \033[1mPress 'q' to stop playback\033[0m\n");
system(play_command);
printf("\033[?25h");
}
else if (user_input[0]=='r' && !user_input[1])
{
random_max = db_file_line_count;
numerical_input=rand() % random_max;
numerical_input++;
PlayStation(numerical_input);
}
else if (user_input[0]=='q' && !user_input[1])
{
printf("\n\n");
exit(0);
}
else if ((isdigit(user_input[0]) && !user_input[1]) ||
(isdigit(user_input[0]) && isdigit(user_input[1]) && !user_input[2]))
{
numerical_input = atoi(user_input);
if (numerical_input > 0 && numerical_input <= db_file_line_count)
{
PlayStation(numerical_input);
}
}
Menu();
return 0;
}
/****************************************************************************/
int DisplayStationList()
{
char buffer[CSV_LIN_MAX_LEN];
char station_name[SN_MAX_LEN];
char array_of_stations[MAX_STATIONS][CSV_LIN_MAX_LEN];
int i;
int x;
int rmndr;
int rows;
int col1;
int col2;
int col3;
const int column_width_1 = 3;
const int column_width_2 = 27;
/* Using ("%s", db_file_location) was suggested by Akshayanti, Stack Overflow,
https://stackoverflow.com/questions/16425148/c-fopen-call-with-variable-name */
FILE *fpointer;
fpointer = fopen(("%s", db_file_location), "r");
x = 0;
/* Loop through file with line length set */
while (fgets(buffer, sizeof(buffer), fpointer))
{
strcpy (array_of_stations[x],buffer);
x++;
}
fclose(fpointer);
db_file_line_count = x;
i = 0;
if (x <= 12)
{
for (i=0; i 12 && x < 25)
{
for (i=0; i 24)
{
rmndr = x % 3;
rows, col1, col2, col3;
for (i=0; i x)
{
printf("%*d %-*s %*d %-*s\n",
column_width_1, col1,
column_width_2, array_of_stations[col1-1],
column_width_1, col2,
column_width_2, array_of_stations[col2-1]);
} else
{
printf("%*d %-*s %*d %-*s %*d %-*s\n",
column_width_1, col1,
column_width_2, array_of_stations[col1-1],
column_width_1, col2,
column_width_2, array_of_stations[col2-1],
column_width_1, col3,
column_width_2, array_of_stations[col3-1]);
}
col1 = col1 + 1;
col2 = col2 + 1;
col3 = col3 + 1;
}
}
return 0;
}
/****************************************************************************/
/* This function removes characters in excess of the variable length from
the user input buffer. If you don't do this, any excess characters will be
entered into the next fgets call. Note that since you're working with a copy
of the variable here, if you have to replace the newline character with a
'/0' in the original variable, you do it _after_ calling this function. */
int ClearInput(char *station_number_copy)
{
// char *p;
// if (p = strchr(station_number_copy, '\n'))
if (strchr(station_number_copy, '\n'))
{
// p = 0;
return 0;
}
else
{
scanf("%*[^\n]");
scanf("%*c");
}
return 0;
}
/****************************************************************************/
int ValidateNumericalInput(char *station_number_copy, int station_number_integer)
{
if ((isdigit(station_number_copy[0]) && station_number_copy[1] == '\n') ||
(isdigit(station_number_copy[0]) && isdigit(station_number_copy[1]) &&
station_number_copy[2] == '\n'))
{
if (station_number_integer < 0 ||
station_number_integer > db_file_line_count - 1)
{
Menu();
}
}
else
{
Menu();
}
return 0;
}
/****************************************************************************/
int PlayStation(int stream_number)
{
char buffer[CSV_LIN_MAX_LEN];
char *array_ptr[MAX_STATIONS];
char station_name[SN_MAX_LEN];
char station_URL[URL_MAX_LEN];
char play_command[137];
char *token;
int x = 0;
FILE *fpointer;
fpointer = fopen(("%s", db_file_location), "r");
while (fgets(buffer, CSV_LIN_MAX_LEN, fpointer))
{
x++;
array_ptr[x]=buffer;
if (x==stream_number)
{
token = strtok(array_ptr[x], s);
strcpy(station_name,token);
token = strtok(NULL, s);
strcpy(station_URL,token);
strcpy(play_command, "gst123 -q ");
strcat(play_command,station_URL);
if ((station_URL[0] != 'h') ||
(station_URL[1] != 't') ||
(station_URL[2] != 't') ||
(station_URL[3] != 'p'))
{
Menu();
}
printf("\033[?25l"); // makes cursor invisible
printf("\n Now Playing - \033[32m%s\033[0m\n\n",station_name);
printf(" \033[1mPress 'q' to stop playback\033[0m\n");
system(play_command);
printf("\033[?25h"); // restores cursor visibility
}
}
fclose(fpointer);
return 0;
}
/****************************************************************************/
int AddStation()
{
char *p;
char csv_line[CSV_LIN_MAX_LEN];
char new_station_name[SN_MAX_LEN];
char new_station_URL[URL_MAX_LEN];
char atoken[2]=",\0";
if (db_file_line_count == 48)
{
printf("\n Maximum number of stations reached.");
fflush(stdout);
sleep(3);
return 0;
}
printf("\n Enter the station name: ");
fgets(new_station_name, sizeof(new_station_name), stdin);
/* Screen out empty input */
if (new_station_name[0] == '\n')
{
return 0;
}
ClearInput(new_station_name);
new_station_name[strcspn(new_station_name, "\n")] = 0;
printf("\n Enter the station URL: ");
fgets(new_station_URL, sizeof(new_station_URL), stdin);
if (new_station_URL[0] == '\n')
{
return 0;
}
ClearInput(new_station_URL);
new_station_URL[strcspn(new_station_URL, "\n")] = 0;
/* Create the new line to add to the file */
strcpy(csv_line,new_station_name);
strcat(csv_line,atoken);
strcat(csv_line,new_station_URL);
strcat(csv_line,"\n");
/* open the file and append the line to it. */
FILE *fpointer;
fpointer = fopen(("%s", db_file_location), "a");
fputs(csv_line,fpointer);
fclose(fpointer);
return 0;
}
/****************************************************************************/
int DeleteStation(int station_number_integer)
{
int i;
int x;
char line[MAX_STATIONS][CSV_LIN_MAX_LEN]; // Two-dimensional array.
/* Read each line from the file into the array
and get the total number of lines. */
FILE *fpointer;
fpointer = fopen(("%s", db_file_location), "r");
x = 0;
while (fgets(line[x], CSV_LIN_MAX_LEN, fpointer))
{
x++;
}
fclose(fpointer);
/* Write each line back to the file except the deletion target. */
fpointer = fopen(("%s", db_file_location), "w");
for (i=0; i