A terminal-based radio player
git clone git://soophie.de/rx
1
CC=cc2
CFLAGS=-Werror -Wall -Wextra -pedantic3
MODFLAGS=-shared -fPIC4
LIBS=-lraylib -lmpv5
DEBUG=-g -rdynamic7
build: src/*.c8
mkdir -p ./build9
$(CC) -o ./build/rx $(CFLAGS) $(LIBS) $(DEBUG) $^11
build-byb: mods/byb.c12
$(CC) -o ./build/byb.so $(CFLAGS) $(MODFLAGS) $(DEBUG) -lcurl $^14
install: build15
cp build/rx ~/.local/bin/rx17
run: build18
./build/rx20
clean:21
rm -rf ./build
1
# rx3
A terminal-based radio player
1
/* inih -- simple .INI file parser3
SPDX-License-Identifier: BSD-3-Clause5
Copyright (C) 2009-2020, Ben Hoyt7
inih is released under the New BSD license (see LICENSE.txt). Go to the project8
home page for more info:10
https://github.com/benhoyt/inih12
*/14
#ifndef INI_H15
#define INI_H17
/* Make this header file easier to include in C++ code */18
#ifdef __cplusplus19
extern "C" {20
#endif22
#include <stdio.h>24
/* Nonzero if ini_handler callback should accept lineno parameter. */25
#ifndef INI_HANDLER_LINENO26
#define INI_HANDLER_LINENO 027
#endif29
/* Visibility symbols, required for Windows DLLs */30
#ifndef INI_API31
#if defined _WIN32 || defined __CYGWIN__32
# ifdef INI_SHARED_LIB33
# ifdef INI_SHARED_LIB_BUILDING34
# define INI_API __declspec(dllexport)35
# else36
# define INI_API __declspec(dllimport)37
# endif38
# else39
# define INI_API40
# endif41
#else42
# if defined(__GNUC__) && __GNUC__ >= 443
# define INI_API __attribute__ ((visibility ("default")))44
# else45
# define INI_API46
# endif47
#endif48
#endif50
/* Typedef for prototype of handler function. */51
#if INI_HANDLER_LINENO52
typedef int (*ini_handler)(void* user, const char* section,53
const char* name, const char* value,54
int lineno);55
#else56
typedef int (*ini_handler)(void* user, const char* section,57
const char* name, const char* value);58
#endif60
/* Typedef for prototype of fgets-style reader function. */61
typedef char* (*ini_reader)(char* str, int num, void* stream);63
/* Parse given INI-style file. May have [section]s, name=value pairs64
(whitespace stripped), and comments starting with ';' (semicolon). Section65
is "" if name=value pair parsed before any section heading. name:value66
pairs are also supported as a concession to Python's configparser.68
For each name=value pair parsed, call handler function with given user69
pointer as well as section, name, and value (data only valid for duration70
of handler call). Handler should return nonzero on success, zero on error.72
Returns 0 on success, line number of first error on parse error (doesn't73
stop on first error), -1 on file open error, or -2 on memory allocation74
error (only when INI_USE_STACK is zero).75
*/76
INI_API int ini_parse(const char* filename, ini_handler handler, void* user);78
/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't79
close the file when it's finished -- the caller must do that. */80
INI_API int ini_parse_file(FILE* file, ini_handler handler, void* user);82
/* Same as ini_parse(), but takes an ini_reader function pointer instead of83
filename. Used for implementing custom or string-based I/O (see also84
ini_parse_string). */85
INI_API int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,86
void* user);88
/* Same as ini_parse(), but takes a zero-terminated string with the INI data89
instead of a file. Useful for parsing INI data from a network socket or90
already in memory. */91
INI_API int ini_parse_string(const char* string, ini_handler handler, void* user);93
/* Nonzero to allow multi-line value parsing, in the style of Python's94
configparser. If allowed, ini_parse() will call the handler with the same95
name for each subsequent line parsed. */96
#ifndef INI_ALLOW_MULTILINE97
#define INI_ALLOW_MULTILINE 198
#endif100
/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of101
the file. See https://github.com/benhoyt/inih/issues/21 */102
#ifndef INI_ALLOW_BOM103
#define INI_ALLOW_BOM 1104
#endif106
/* Chars that begin a start-of-line comment. Per Python configparser, allow107
both ; and # comments at the start of a line by default. */108
#ifndef INI_START_COMMENT_PREFIXES109
#define INI_START_COMMENT_PREFIXES ";#"110
#endif112
/* Nonzero to allow inline comments (with valid inline comment characters113
specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match114
Python 3.2+ configparser behaviour. */115
#ifndef INI_ALLOW_INLINE_COMMENTS116
#define INI_ALLOW_INLINE_COMMENTS 1117
#endif118
#ifndef INI_INLINE_COMMENT_PREFIXES119
#define INI_INLINE_COMMENT_PREFIXES ";"120
#endif122
/* Nonzero to use stack for line buffer, zero to use heap (malloc/free). */123
#ifndef INI_USE_STACK124
#define INI_USE_STACK 1125
#endif127
/* Maximum line length for any line in INI file (stack or heap). Note that128
this must be 3 more than the longest line (due to '\r', '\n', and '\0'). */129
#ifndef INI_MAX_LINE130
#define INI_MAX_LINE 200131
#endif133
/* Nonzero to allow heap line buffer to grow via realloc(), zero for a134
fixed-size buffer of INI_MAX_LINE bytes. Only applies if INI_USE_STACK is135
zero. */136
#ifndef INI_ALLOW_REALLOC137
#define INI_ALLOW_REALLOC 0138
#endif140
/* Initial size in bytes for heap line buffer. Only applies if INI_USE_STACK141
is zero. */142
#ifndef INI_INITIAL_ALLOC143
#define INI_INITIAL_ALLOC 200144
#endif146
/* Stop parsing on first error (default is to keep parsing). */147
#ifndef INI_STOP_ON_FIRST_ERROR148
#define INI_STOP_ON_FIRST_ERROR 0149
#endif151
/* Nonzero to call the handler at the start of each new section (with152
name and value NULL). Default is to only call the handler on153
each name=value pair. */154
#ifndef INI_CALL_HANDLER_ON_NEW_SECTION155
#define INI_CALL_HANDLER_ON_NEW_SECTION 0156
#endif158
/* Nonzero to allow a name without a value (no '=' or ':' on the line) and159
call the handler with value NULL in this case. Default is to treat160
no-value lines as an error. */161
#ifndef INI_ALLOW_NO_VALUE162
#define INI_ALLOW_NO_VALUE 0163
#endif165
/* Nonzero to use custom ini_malloc, ini_free, and ini_realloc memory166
allocation functions (INI_USE_STACK must also be 0). These functions must167
have the same signatures as malloc/free/realloc and behave in a similar168
way. ini_realloc is only needed if INI_ALLOW_REALLOC is set. */169
#ifndef INI_CUSTOM_ALLOCATOR170
#define INI_CUSTOM_ALLOCATOR 0171
#endif174
#ifdef __cplusplus175
}176
#endif178
#ifdef INI_IMPL180
#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)181
#define _CRT_SECURE_NO_WARNINGS182
#endif184
#include <stdio.h>185
#include <ctype.h>186
#include <string.h>188
#include "ini.h"190
#if !INI_USE_STACK191
#if INI_CUSTOM_ALLOCATOR192
#include <stddef.h>193
void* ini_malloc(size_t size);194
void ini_free(void* ptr);195
void* ini_realloc(void* ptr, size_t size);196
#else197
#include <stdlib.h>198
#define ini_malloc malloc199
#define ini_free free200
#define ini_realloc realloc201
#endif202
#endif204
#define MAX_SECTION 50205
#define MAX_NAME 50207
/* Used by ini_parse_string() to keep track of string parsing state. */208
typedef struct {209
const char* ptr;210
size_t num_left;211
} ini_parse_string_ctx;213
/* Strip whitespace chars off end of given string, in place. Return s. */214
static char* ini_rstrip(char* s)215
{216
char* p = s + strlen(s);217
while (p > s && isspace((unsigned char)(*--p)))218
*p = '\0';219
return s;220
}222
/* Return pointer to first non-whitespace char in given string. */223
static char* ini_lskip(const char* s)224
{225
while (*s && isspace((unsigned char)(*s)))226
s++;227
return (char*)s;228
}230
/* Return pointer to first char (of chars) or inline comment in given string,231
or pointer to NUL at end of string if neither found. Inline comment must232
be prefixed by a whitespace character to register as a comment. */233
static char* ini_find_chars_or_comment(const char* s, const char* chars)234
{235
#if INI_ALLOW_INLINE_COMMENTS236
int was_space = 0;237
while (*s && (!chars || !strchr(chars, *s)) &&238
!(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) {239
was_space = isspace((unsigned char)(*s));240
s++;241
}242
#else243
while (*s && (!chars || !strchr(chars, *s))) {244
s++;245
}246
#endif247
return (char*)s;248
}250
/* Similar to strncpy, but ensures dest (size bytes) is251
NUL-terminated, and doesn't pad with NULs. */252
static char* ini_strncpy0(char* dest, const char* src, size_t size)253
{254
/* Could use strncpy internally, but it causes gcc warnings (see issue #91) */255
size_t i;256
for (i = 0; i < size - 1 && src[i]; i++)257
dest[i] = src[i];258
dest[i] = '\0';259
return dest;260
}262
/* See documentation in header file. */263
int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,264
void* user)265
{266
/* Uses a fair bit of stack (use heap instead if you need to) */267
#if INI_USE_STACK268
char line[INI_MAX_LINE];269
size_t max_line = INI_MAX_LINE;270
#else271
char* line;272
size_t max_line = INI_INITIAL_ALLOC;273
#endif274
#if INI_ALLOW_REALLOC && !INI_USE_STACK275
char* new_line;276
size_t offset;277
#endif278
char section[MAX_SECTION] = "";279
#if INI_ALLOW_MULTILINE280
char prev_name[MAX_NAME] = "";281
#endif283
char* start;284
char* end;285
char* name;286
char* value;287
int lineno = 0;288
int error = 0;290
#if !INI_USE_STACK291
line = (char*)ini_malloc(INI_INITIAL_ALLOC);292
if (!line) {293
return -2;294
}295
#endif297
#if INI_HANDLER_LINENO298
#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno)299
#else300
#define HANDLER(u, s, n, v) handler(u, s, n, v)301
#endif303
/* Scan through stream line by line */304
while (reader(line, (int)max_line, stream) != NULL) {305
#if INI_ALLOW_REALLOC && !INI_USE_STACK306
offset = strlen(line);307
while (offset == max_line - 1 && line[offset - 1] != '\n') {308
max_line *= 2;309
if (max_line > INI_MAX_LINE)310
max_line = INI_MAX_LINE;311
new_line = ini_realloc(line, max_line);312
if (!new_line) {313
ini_free(line);314
return -2;315
}316
line = new_line;317
if (reader(line + offset, (int)(max_line - offset), stream) == NULL)318
break;319
if (max_line >= INI_MAX_LINE)320
break;321
offset += strlen(line + offset);322
}323
#endif325
lineno++;327
start = line;328
#if INI_ALLOW_BOM329
if (lineno == 1 && (unsigned char)start[0] == 0xEF &&330
(unsigned char)start[1] == 0xBB &&331
(unsigned char)start[2] == 0xBF) {332
start += 3;333
}334
#endif335
start = ini_rstrip(ini_lskip(start));337
if (strchr(INI_START_COMMENT_PREFIXES, *start)) {338
/* Start-of-line comment */339
}340
#if INI_ALLOW_MULTILINE341
else if (*prev_name && *start && start > line) {342
#if INI_ALLOW_INLINE_COMMENTS343
end = ini_find_chars_or_comment(start, NULL);344
if (*end)345
*end = '\0';346
ini_rstrip(start);347
#endif348
/* Non-blank line with leading whitespace, treat as continuation349
of previous name's value (as per Python configparser). */350
if (!HANDLER(user, section, prev_name, start) && !error)351
error = lineno;352
}353
#endif354
else if (*start == '[') {355
/* A "[section]" line */356
end = ini_find_chars_or_comment(start + 1, "]");357
if (*end == ']') {358
*end = '\0';359
ini_strncpy0(section, start + 1, sizeof(section));360
#if INI_ALLOW_MULTILINE361
*prev_name = '\0';362
#endif363
#if INI_CALL_HANDLER_ON_NEW_SECTION364
if (!HANDLER(user, section, NULL, NULL) && !error)365
error = lineno;366
#endif367
}368
else if (!error) {369
/* No ']' found on section line */370
error = lineno;371
}372
}373
else if (*start) {374
/* Not a comment, must be a name[=:]value pair */375
end = ini_find_chars_or_comment(start, "=:");376
if (*end == '=' || *end == ':') {377
*end = '\0';378
name = ini_rstrip(start);379
value = end + 1;380
#if INI_ALLOW_INLINE_COMMENTS381
end = ini_find_chars_or_comment(value, NULL);382
if (*end)383
*end = '\0';384
#endif385
value = ini_lskip(value);386
ini_rstrip(value);388
#if INI_ALLOW_MULTILINE389
ini_strncpy0(prev_name, name, sizeof(prev_name));390
#endif391
/* Valid name[=:]value pair found, call handler */392
if (!HANDLER(user, section, name, value) && !error)393
error = lineno;394
}395
else if (!error) {396
/* No '=' or ':' found on name[=:]value line */397
#if INI_ALLOW_NO_VALUE398
*end = '\0';399
name = ini_rstrip(start);400
if (!HANDLER(user, section, name, NULL) && !error)401
error = lineno;402
#else403
error = lineno;404
#endif405
}406
}408
#if INI_STOP_ON_FIRST_ERROR409
if (error)410
break;411
#endif412
}414
#if !INI_USE_STACK415
ini_free(line);416
#endif418
return error;419
}421
/* See documentation in header file. */422
int ini_parse_file(FILE* file, ini_handler handler, void* user)423
{424
return ini_parse_stream((ini_reader)fgets, file, handler, user);425
}427
/* See documentation in header file. */428
int ini_parse(const char* filename, ini_handler handler, void* user)429
{430
FILE* file;431
int error;433
file = fopen(filename, "r");434
if (!file)435
return -1;436
error = ini_parse_file(file, handler, user);437
fclose(file);438
return error;439
}441
/* An ini_reader function to read the next line from a string buffer. This442
is the fgets() equivalent used by ini_parse_string(). */443
static char* ini_reader_string(char* str, int num, void* stream) {444
ini_parse_string_ctx* ctx = (ini_parse_string_ctx*)stream;445
const char* ctx_ptr = ctx->ptr;446
size_t ctx_num_left = ctx->num_left;447
char* strp = str;448
char c;450
if (ctx_num_left == 0 || num < 2)451
return NULL;453
while (num > 1 && ctx_num_left != 0) {454
c = *ctx_ptr++;455
ctx_num_left--;456
*strp++ = c;457
if (c == '\n')458
break;459
num--;460
}462
*strp = '\0';463
ctx->ptr = ctx_ptr;464
ctx->num_left = ctx_num_left;465
return str;466
}468
/* See documentation in header file. */469
int ini_parse_string(const char* string, ini_handler handler, void* user) {470
ini_parse_string_ctx ctx;472
ctx.ptr = string;473
ctx.num_left = strlen(string);474
return ini_parse_stream((ini_reader)ini_reader_string, &ctx, handler,475
user);476
}478
#endif /* INI_IMPL */479
#endif /* INI_H */
1
#pragma once3
#include <stdbool.h>4
#include <time.h>5
#include <mpv/client.h>7
#define RX_UPDATE_COUNT 509
typedef struct {10
char uid[256];11
char name[256];12
char *addr;13
char *url;14
char *resolver;15
} rx_station_t;17
typedef struct {18
bool quit;19
rx_station_t *stations;20
size_t stations_len;21
const rx_station_t *curr;22
mpv_handle *ctx;23
int idx;24
int count;25
char *title;26
int loading;27
int error;28
char message[500];29
} rx_t;31
void rx_init(rx_t *self);32
void rx_update(rx_t *self);33
void rx_draw(rx_t *self);34
void rx_free(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_BYB_URL "https://playerservices.streamtheworld.com/api/livestream?station=BYB_RADIO&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_BYB_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, "/BYB_RADIO.mp3");45
break;46
}47
}48
free(curl_data);49
return url;50
}
1
[stations]2
uid=BYB3
name=Backyard Bend Radio4
addr=Bend, US5
resolver=build/byb.so7
[stations]8
uid=RPD9
name=Radio Paradise10
addr=Eur11
url=https://stream.radioparadise.com/aac-32013
[stations]14
uid=BR215
name=BBC Radio 216
addr=London, UK17
url=https://as-hls-ww-live.akamaized.net/pool_904/live/ww/bbc_radio_two/bbc_radio_two.isml/bbc_radio_two-audio%3d96000.norewind.m3u819
[stations]20
uid=PWR21
name=Pure West Radio22
addr=Pembroke, UK23
url=https://streaming.broadcastradio.com:8582/purewest25
[stations]26
uid=BWV27
name=BAY WAVE28
addr=Shiogama, JP29
url=https://mtist.as.smartstream.ne.jp/30056/livestream/playlist.m3u831
[stations]32
uid=KSW33
name=Kishiwada34
addr=Kishiwada, JP35
url=http://61.89.201.27:8000/radikishi.mp3"37
[stations]38
uid=KBO39
name=KBOO 90.740
addr=Portland, US41
url=http://live.kboo.fm:8000/high.m3u43
[stations]44
uid=WPR45
name=Whisperings Radio46
addr=???47
url=https://pianosolo.streamguys1.com/live?us_privacy=1YNN49
[stations]50
uid=TWV51
name=97.3 The Wave52
addr=St. John, CA53
url=http://mbsradio.leanstream.co/CHNSFM-MP355
[stations]56
uid=COK57
name=Choku 86.158
addr=Nogata, JP59
url=https://mtist.as.smartstream.ne.jp/30085/livestream/playlist.m3u861
[stations]62
uid=NWJ63
name=NHK World Japan64
addr=Tokyo, JP65
url=https://nhkwlive-radio.akamaized.net/hls/live/2032383/radio-rs4/index_06.m3u867
[stations]68
uid=RRM69
name=Radio Record Megamix70
addr=St. Petersburg, RU71
url=https://radiorecord.hostingradio.ru/mix96.aacp73
[stations]74
uid=RAR75
name=Retro Album Rock76
addr=Nashville, US77
url=http://das-edge13-live365-dal02.cdnstream.com/a8982479
[stations]80
uid=GVH81
name=Gravity House82
addr=Montreal, CA83
url=http://176.9.7.145:8114/stream85
[stations]86
uid=AMA87
name=A.M. Ambient88
addr=???89
url=http://radio.stereoscenic.com/ama-h91
[stations]92
uid=J1G93
name=J1 Gold94
addr=Tokyo, JP95
url=https://jenny.torontocast.com:2000/stream/J1GOLD97
[stations]98
uid=J1H99
name=J1 Hits100
addr=Tokyo, JP101
url=https://jenny.torontocast.com:2000/stream/J1HITS103
[stations]104
uid=TOT105
name=KPPT 100.7 The Otter106
addr=Newport, US107
url=https://pacnw.streamguys1.com/live109
[stations]110
uid=BOW111
name=Blown112
addr=Milverton, CA113
url=http://crucialvelocity.ca/blown128.m3u115
[stations]116
uid=CHR117
name=CHQR 770118
addr=Calgary, CA119
url=http://live.leanstream.co/CHQRAM121
[stations]122
uid=CKQ123
name=CKQR 99.3 The Goat124
addr=Castlegar, CA125
url=https://vistaradio.streamb.live/SB00085127
[stations]128
uid=ASY129
name=Ashiya Radio130
addr=Ashiya, JP131
url=https://s3.radio.co/sc8d895604/listen133
[stations]134
uid=NTG135
name=Nostalgie136
addr=Paris, FR137
url=https://scdn.nrjaudio.fm/fr/40106/aac_64.mp3?cdn_path=audio_lbs12&access_token=427b54a3cea144ba93dd6dfe429e45a4139
[stations]140
uid=NWP141
name=Nightwave Plaza142
addr=???143
url=http://radio.plaza.one/mp3
1
#include <stdio.h>2
#include <stdlib.h>3
#include <signal.h>4
#include <execinfo.h>5
#include <unistd.h>7
#include "../include/rx.h"9
#define BACKTRACE_SIZE 1011
void handle_segfault(int signal) {12
void *list[BACKTRACE_SIZE];13
size_t size = backtrace(list, BACKTRACE_SIZE);14
fprintf(stderr, "ERROR: signal %d:\n", signal);15
backtrace_symbols_fd(list, size, STDERR_FILENO);16
exit(EXIT_FAILURE);17
}19
rx_t rx;21
void do_update(void) {22
rx_update(&rx);23
rx_draw(&rx);24
}26
int main(void) {27
rx_init(&rx);28
struct sigaction sigint_action;29
sigint_action.sa_handler = handle_segfault;30
sigint_action.sa_flags = 0;31
sigemptyset(&sigint_action.sa_mask);32
sigaction(SIGSEGV, &sigint_action, NULL);33
while (!rx.quit) {34
do_update();35
}36
rx_free(&rx);37
return EXIT_SUCCESS;38
}
1
#include <stdio.h>2
#include <stdlib.h>3
#include <string.h>4
#include <dlfcn.h>5
#include <math.h>6
#include <mpv/client.h>7
#define LIB_TERM_IMPL8
#include <libterm.h>9
#define LIB_STR_IMPL10
#include <libstr.h>12
#include "../include/rx.h"13
#define INI_IMPL14
#include "../include/ini.h"16
static int rx_ini_load(void *user, const char *section, const char *name, const char *value) {17
rx_t *self = (rx_t *) user;18
if (strcmp(section, "stations") == 0) {19
if (strcmp(name, "uid") == 0) {20
self->stations = realloc(self->stations, sizeof(rx_station_t) * (self->stations_len + 1));21
self->stations[self->stations_len] = (rx_station_t) {22
.addr = NULL,23
.url = NULL,24
.resolver = NULL,25
};26
self->stations_len++;27
}28
rx_station_t *station = &self->stations[self->stations_len - 1];29
if (strcmp(name, "uid") == 0) {30
size_t len = strlen(value);31
memcpy(station->uid, value, len);32
station->uid[len] = '\0';33
}34
if (strcmp(name, "name") == 0) {35
size_t len = strlen(value);36
memcpy(station->name, value, len);37
station->name[len] = '\0';38
}39
if (strcmp(name, "addr") == 0) {40
size_t len = strlen(value);41
station->addr = malloc(sizeof(char) * (len + 1));42
memcpy(station->addr, value, len);43
station->addr[len] = '\0';44
}45
if (strcmp(name, "url") == 0) {46
size_t len = strlen(value);47
station->url = malloc(sizeof(char) * (len + 1));48
memcpy(station->url, value, len);49
station->url[len] = '\0';50
}51
if (strcmp(name, "resolver") == 0) {52
size_t len = strlen(value);53
station->resolver = malloc(sizeof(char) * (len + 1));54
memcpy(station->resolver, value, len);55
station->resolver[len] = '\0';56
}57
}58
return 1;59
}61
void rx_init(rx_t *self) {62
self->stations = NULL;63
self->stations_len = 0;64
char *config_path = NULL;65
char *home_path = getenv("HOME");66
if (home_path != NULL) {67
str_appendf(&config_path, "%s/.config/rx/", home_path);68
}69
str_append(&config_path, "rx.ini");70
ini_parse(config_path, rx_ini_load, self);71
free(config_path);72
self->curr = NULL;73
self->ctx = mpv_create();74
mpv_initialize(self->ctx);75
self->idx = 0;76
self->count = 0;77
self->title = NULL;78
self->loading = 0;79
self->error = 0;80
term_enable_raw_mode();81
term_write("\x1b[?25l");82
}84
void rx_update(rx_t *self) {85
int key = term_poll_key(100);86
switch (key) {87
case 'q': {88
self->quit = true;89
break;90
}91
case 'j': {92
if ((size_t) self->idx < self->stations_len - 1) {93
self->idx++;94
}95
break;96
}97
case 'k': {98
if (self->idx > 0) {99
self->idx--;100
}101
break;102
}103
case KEY_ENTER: {104
const rx_station_t *station = &self->stations[self->idx];105
char *resolver = station->resolver;106
char *url = NULL;107
if (resolver != NULL) {108
char *resolver_path = NULL;109
char *home_path = getenv("HOME");110
if (home_path != NULL) {111
str_appendf(&resolver_path, "%s/.config/rx/mods/", home_path);112
}113
str_append(&resolver_path, resolver);114
void *handle = dlopen(resolver_path, RTLD_LAZY);115
free(resolver_path);116
if (handle == NULL) {117
fprintf(stderr, "%s\n", dlerror());118
break;119
}120
char *(*resolve_fn)(void) = (char *(*)(void)) dlsym(handle, "rx_resolve");121
const char *err = dlerror();122
if (err != 0) {123
fprintf(stderr, "%s\n", err);124
break;125
}126
url = resolve_fn();127
dlclose(handle);128
}129
else {130
size_t len = strlen(station->url);131
url = malloc(sizeof(char) * (len + 1));132
memcpy(url, station->url, len);133
url[len] = '\0';134
}135
if (url != NULL) {136
const char *cmd[] = { "loadfile", url, NULL };137
mpv_command(self->ctx, cmd);138
free(url);139
}140
self->curr = station;141
self->title = NULL;142
self->loading = 1;143
self->error = 0;144
break;145
}146
case KEY_BACKSPACE: {147
const char *cmd[] = { "stop", NULL };148
mpv_command(self->ctx, cmd);149
self->curr = NULL;150
self->loading = 0;151
self->error = 0;152
break;153
}154
default:155
break;156
}157
if (self->count == RX_UPDATE_COUNT) {158
if (self->title != NULL) {159
mpv_free(self->title);160
self->title = NULL;161
}162
self->title = mpv_get_property_string(self->ctx, "metadata/icy-title");163
self->count = 0;164
}165
self->count++;166
mpv_get_property(self->ctx, "core-idle", MPV_FORMAT_FLAG, &self->loading);167
mpv_event *event = mpv_wait_event(self->ctx, 0);168
if (event->event_id == MPV_EVENT_END_FILE) {169
mpv_event_end_file *end_file = (mpv_event_end_file *) event->data;170
if (end_file->reason == MPV_END_FILE_REASON_ERROR) {171
self->error = 1;172
const char* err = mpv_error_string(end_file->error);173
size_t len = strlen(err);174
memcpy(self->message, err, len);175
self->message[len] = '\0';176
}177
}178
}180
void rx_draw(rx_t *self) {181
term_write("\x1b[2J");182
term_write("\x1b[H");183
term_write("\r\n");184
for (size_t i = 0; i < self->stations_len; i++) {185
const rx_station_t *station = &self->stations[i];186
if (i == (size_t) self->idx) {187
term_write("> ");188
}189
else {190
term_write(" ");191
}192
int pad = 3; // floor(log10(self->stations_len) + 1);193
term_writef("%0*d ", pad, i + 1);194
term_writef("[%s] %s", station->uid, station->name);195
if (station->addr != NULL) {196
term_writef(" (%s)", station->addr);197
}198
term_write("\r\n");199
}200
term_write("\r\n");201
const rx_station_t *station = self->curr;202
if (station != NULL) {203
term_writef("[%s] %s\r\n", station->uid, station->name);204
if (self->error) {205
term_writef("error: %s\r\n", self->message);206
}207
else {208
if (self->loading) {209
term_write("loading...\r\n");210
}211
else {212
if (self->title != NULL && strlen(self->title) > 0) {213
term_writef("title: %s\r\n", self->title);214
}215
else {216
term_write("title: ???\r\n");217
}218
}219
}220
}221
term_flush();222
}224
void rx_free(rx_t *self) {225
mpv_terminate_destroy(self->ctx);226
term_write("\x1b[2J");227
term_write("\x1b[H");228
term_write("\x1b[?25h");229
term_flush();230
term_disable_raw_mode();231
for (size_t i = 0; i < self->stations_len; i++) {232
rx_station_t *station = &self->stations[i];233
if (station->addr != NULL) {234
free(station->addr);235
}236
if (station->url != NULL) {237
free(station->url);238
}239
if (station->resolver != NULL) {240
free(station->resolver);241
}242
}243
free(self->stations);244
self->stations_len = 0;245
}