A terminal-based radio player
git clone git://soophie.de/rx
1
CC=cc2
CFLAGS=-Werror -Wall -Wextra -pedantic3
MODFLAGS=-shared -fPIC4
LIBS=-lraylib -lmpv4
LIBS=-lmpv5
DEBUG=-g -rdynamic7
build: src/*.c
11
build-byb: mods/byb.c12
$(CC) -o ./build/byb.so $(CFLAGS) $(MODFLAGS) $(DEBUG) -lcurl $^14
build-xcr: mods/xcr.c15
$(CC) -o ./build/xcr.so $(CFLAGS) $(MODFLAGS) $(DEBUG) -lcurl $^17
install: build18
cp build/rx ~/.local/bin/rx20
install-mods: build-byb build-xcr21
cp build/*.so ~/.config/rx/mods/23
run: build24
./build/rx
3
#include <stdbool.h>4
#include <time.h>5
#include <mpv/client.h>6
#include <so/term.h>8
#define RX_UPDATE_COUNT 50
27
int loading;28
int error;29
char message[500];30
term_t term;31
} rx_t;33
void rx_init(rx_t *self);
1
#include <stdlib.h>2
#include <string.h>3
#include <curl/curl.h>4
#define LIB_STR_IMPL5
#include <libstr.h>7
#define RX_XCR_URL "https://playerservices.streamtheworld.com/api/livestream?station=KMGXFM&transports=http%2Chls&version=1.10&request.preventCache=1735994936887"9
size_t rx_curl_write(void *ptr, size_t size, size_t nmemb, void *data) {10
size_t len = size * nmemb;11
char **curl_data = (char **) data;12
str_append_len(curl_data, ptr, len);13
return len;14
}16
char *rx_resolve(void) {17
CURL *curl = curl_easy_init();18
if (!curl) {19
fprintf(stderr, "CURL init failed!\n");20
return NULL;21
}22
char *curl_data = NULL;23
curl_easy_setopt(curl, CURLOPT_URL, RX_XCR_URL);24
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, rx_curl_write);25
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &curl_data);26
CURLcode code = curl_easy_perform(curl);27
if (code != CURLE_OK) {28
curl_easy_cleanup(curl);29
fprintf(stderr, "CURL fetch failed!\n%s\n", curl_easy_strerror(code));30
return NULL;31
}32
curl_easy_cleanup(curl);33
char *url = NULL;34
size_t set = 0;35
for (size_t i = 0; i < strlen(curl_data); i++) {36
if(strncmp(curl_data + i, "<ip>", 4) == 0) {37
set = i + 4;38
continue;39
}40
if (set && curl_data[i] == '<') {41
size_t len = i - set;42
str_append(&url, "https://");43
str_append_len(&url, curl_data + set, len);44
str_append(&url, "/KMGXFM.mp3");45
break;46
}47
}48
free(curl_data);49
return url;50
}
9
#define BACKTRACE_SIZE 1011
void handle_segfault(int signal) {11
void on_segfault(int signal) {12
void *list[BACKTRACE_SIZE];13
size_t size = backtrace(list, BACKTRACE_SIZE);14
fprintf(stderr, "ERROR: signal %d:\n", signal);
23
rx_draw(&rx);24
}26
void cleanup(void) {27
rx_free(&rx);28
}30
int main(void) {31
rx_init(&rx);32
struct sigaction sigint_action;29
sigint_action.sa_handler = handle_segfault;33
sigint_action.sa_handler = on_segfault;34
sigint_action.sa_flags = 0;35
sigemptyset(&sigint_action.sa_mask);36
sigaction(SIGSEGV, &sigint_action, NULL);37
atexit(cleanup);38
while (!rx.quit) {39
do_update();40
}36
rx_free(&rx);41
return EXIT_SUCCESS;42
}
2
#include <stdlib.h>3
#include <string.h>4
#include <dlfcn.h>5
#include <math.h>5
#include <mpv/client.h>6
#define LIB_TERM_IMPL8
#include <libterm.h>7
#define LIB_TERM_POSIX8
#include <so/term.h>9
#define LIB_STR_IMPL10
#include <libstr.h>
66
if (home_path != NULL) {67
str_appendf(&config_path, "%s/.config/rx/", home_path);68
}69
str_append(&config_path, "rx.ini");69
str_append(&config_path, "lst.ini");70
ini_parse(config_path, rx_ini_load, self);71
free(config_path);72
self->curr = NULL;
77
self->title = NULL;78
self->loading = 0;79
self->error = 0;80
term_enable_raw_mode();81
term_write("\x1b[?25l");80
self->term = term_init();81
term_rawmode_enable(&self->term);82
term_write(&self->term, "\x1b[?25l");83
}85
void rx_update(rx_t *self) {85
int key = term_poll_key(100);86
int key = term_poll_key(&self->term, 100);87
switch (key) {88
case 'q': {89
self->quit = true;
101
}102
break;103
}103
case KEY_ENTER: {104
case ' ': {105
const rx_station_t *station = &self->stations[self->idx];106
char *resolver = station->resolver;107
char *url = NULL;
179
}181
void rx_draw(rx_t *self) {181
term_write("\x1b[2J");182
term_write("\x1b[H");183
term_write("\r\n");184
int rows = 0;185
int cols = 0;186
term_read_window_size(&rows, &cols);182
term_write(&self->term, "\x1b[2J");183
term_write(&self->term, "\x1b[H");184
term_write(&self->term, "\r\n");185
size_t rows = 0;186
size_t cols = 0;187
term_read_window(&self->term, &rows, &cols);188
size_t len = rows - 5;189
for (size_t i = 0; i < len; i++) {190
if (i + self->idx >= self->stations_len) {190
term_write("\r\n");191
term_write(&self->term, "\r\n");192
continue;193
}194
const rx_station_t *station = &self->stations[i + self->idx];195
if (i + self->idx == (size_t) self->idx) {195
term_write("> ");196
term_write(&self->term, "> ");197
}198
else {198
term_write(" ");199
term_write(&self->term, " ");200
}201
int pad = 3; // floor(log10(self->stations_len) + 1);201
term_writef("%0*d ", pad, i + self->idx + 1);202
term_writef("[%s] %s", station->uid, station->name);202
term_writef(&self->term, "%0*d ", pad, i + self->idx + 1);203
term_writef(&self->term, "[%s] %s", station->uid, station->name);204
if (station->addr != NULL) {204
term_writef(" (%s)", station->addr);205
term_writef(&self->term, " (%s)", station->addr);206
}206
term_write("\r\n");207
term_write(&self->term, "\r\n");208
}208
term_write("\r\n");209
term_write(&self->term, "\r\n");210
const rx_station_t *station = self->curr;211
if (station != NULL) {211
term_writef("[%s] %s\r\n", station->uid, station->name);212
term_writef(&self->term, "[%s] %s\r\n", station->uid, station->name);213
if (self->error) {213
term_writef("error: %s\r\n", self->message);214
term_writef(&self->term, "error: %s\r\n", self->message);215
}216
else {217
if (self->loading) {217
term_write("loading...\r\n");218
term_write(&self->term, "loading...\r\n");219
}220
else {221
if (self->title != NULL && strlen(self->title) > 0) {221
term_writef("title: %s\r\n", self->title);222
term_writef(&self->term, "title: %s\r\n", self->title);223
}224
else {224
term_write("title: ???\r\n");225
term_write(&self->term, "title: ???\r\n");226
}227
}228
}229
}230
else {230
term_write("[???] no station\r\n");231
term_write("...\r\n");231
term_write(&self->term, "[???] no station\r\n");232
term_write(&self->term, "...\r\n");233
}233
term_flush();234
term_flush(&self->term);235
}237
void rx_free(rx_t *self) {238
mpv_terminate_destroy(self->ctx);238
term_write("\x1b[2J");239
term_write("\x1b[H");240
term_write("\x1b[?25h");241
term_flush();242
term_disable_raw_mode();239
term_write(&self->term, "\x1b[2J");240
term_write(&self->term, "\x1b[H");241
term_write(&self->term, "\x1b[?25h");242
term_flush(&self->term);243
term_rawmode_disable(&self->term);244
term_cleanup(&self->term);245
for (size_t i = 0; i < self->stations_len; i++) {246
rx_station_t *station = &self->stations[i];247
if (station->addr != NULL) {