diff options
| author | Andrew Kloet <andrew@kloet.net> | 2025-07-03 18:45:25 -0400 |
|---|---|---|
| committer | Andrew Kloet <andrew@kloet.net> | 2025-07-03 18:45:25 -0400 |
| commit | 5025698be7e6f826f539cd105190519bd68c566a (patch) | |
| tree | 772a8fff191cb781c9afd7aac8dc53b6c2774bb4 /mpd_ws.c | |
initial commit
Diffstat (limited to 'mpd_ws.c')
| -rw-r--r-- | mpd_ws.c | 346 |
1 files changed, 346 insertions, 0 deletions
diff --git a/mpd_ws.c b/mpd_ws.c new file mode 100644 index 0000000..f976f8d --- /dev/null +++ b/mpd_ws.c @@ -0,0 +1,346 @@ +#define _GNU_SOURCE +#include "mpd_ws.h" +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/select.h> +#include <syslog.h> +#include <time.h> +#include <unistd.h> + +/* Global server instance for signal handling */ +static struct mpd_ws_server *g_server = NULL; + +/* Signal handler */ +static void signal_handler(int sig) { + (void)sig; /* Suppress unused param warning */ + if (g_server) { + g_server->running = 0; + } +} + +/* WebSocket callback */ +static int ws_callback(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) { + (void)user; /* Suppress unused param warnings */ + (void)in; + (void)len; + + struct mpd_ws_server *server = + (struct mpd_ws_server *)lws_context_user(lws_get_context(wsi)); + + switch (reason) { + case LWS_CALLBACK_ESTABLISHED: + client_add(server, wsi); + /* Send current song to new client */ + if (strlen(server->current_song) > 0) { + lws_callback_on_writable(wsi); + } + break; + + case LWS_CALLBACK_CLOSED: + client_remove(server, wsi); + break; + + case LWS_CALLBACK_SERVER_WRITEABLE: + if (strlen(server->current_song) > 0) { + unsigned char buf[LWS_PRE + MAX_MESSAGE_SIZE]; + size_t msg_len = strlen(server->current_song); + memcpy(&buf[LWS_PRE], server->current_song, msg_len); + lws_write(wsi, &buf[LWS_PRE], msg_len, LWS_WRITE_TEXT); + } + break; + + case LWS_CALLBACK_RECEIVE: + /* Ignore client input */ + break; + + default: + break; + } + + return 0; +} + +/* WebSocket protocols */ +static struct lws_protocols protocols[] = { + {"mpd-protocol", ws_callback, 0, MAX_MESSAGE_SIZE, 0, NULL, 0}, + {NULL, NULL, 0, 0, 0, NULL, 0}}; + +/* Initialize the server */ +int mpd_ws_init(struct mpd_ws_server *server) { + memset(server, 0, sizeof(*server)); + + /* Setup signal handlers */ + g_server = server; + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + + /* Initialize syslog */ + openlog("mpd_ws", LOG_PID | LOG_NDELAY, LOG_DAEMON); + + /* Create WebSocket context */ + struct lws_context_creation_info info = {0}; + info.port = WEBSOCKET_PORT; + info.protocols = protocols; + info.gid = -1; + info.uid = -1; + info.user = server; + + server->ws_context = lws_create_context(&info); + if (!server->ws_context) { + syslog(LOG_ERR, "Failed to create WebSocket context"); + return -1; + } + + syslog(LOG_INFO, "WebSocket server listening on port %d", WEBSOCKET_PORT); + + /* Connect to MPD */ + mpd_ws_connect(server); + + server->running = 1; + return 0; +} + +/* Main server loop */ +void mpd_ws_run(struct mpd_ws_server *server) { + time_t last_reconnect = 0; + + while (server->running) { + /* Service WebSocket events */ + lws_service(server->ws_context, 10); + + /* Handle MPD reconnection */ + if (!mpd_ws_is_connected(server)) { + time_t now = time(NULL); + if (now - last_reconnect >= RECONNECT_INTERVAL_SEC) { + if (mpd_ws_connect(server) == 0) { + mpd_ws_update_song(server); + mpd_ws_start_idle(server); + } + last_reconnect = now; + } + sleep(SELECT_TIMEOUT_SEC); + continue; + } + + /* Start idle mode if not active */ + if (!server->mpd_idle_active) { + if (mpd_ws_start_idle(server) < 0) { + mpd_ws_disconnect(server); + continue; + } + } + + /* Check for MPD events */ + fd_set readfds; + int mpd_fd = mpd_connection_get_fd(server->mpd_conn); + struct timeval timeout = {0, SELECT_TIMEOUT_SEC}; + + FD_ZERO(&readfds); + FD_SET(mpd_fd, &readfds); + + int result = select(mpd_fd + 1, &readfds, NULL, NULL, &timeout); + if (result > 0 && FD_ISSET(mpd_fd, &readfds)) { + mpd_ws_process_idle(server); + if (!mpd_ws_is_connected(server)) { + mpd_ws_disconnect(server); + } + } + } +} + +/* Cleanup and shutdown */ +void mpd_ws_cleanup(struct mpd_ws_server *server) { + syslog(LOG_INFO, "Shutting down"); + + mpd_ws_disconnect(server); + + if (server->ws_context) { + lws_context_destroy(server->ws_context); + } + + /* Free client list */ + while (server->clients) { + struct client_session *next = server->clients->next; + free(server->clients); + server->clients = next; + } + + closelog(); +} + +/* Stop the server */ +void mpd_ws_stop(struct mpd_ws_server *server) { server->running = 0; } + +/* Connect to MPD */ +int mpd_ws_connect(struct mpd_ws_server *server) { + if (server->mpd_conn) { + mpd_connection_free(server->mpd_conn); + } + + server->mpd_conn = mpd_connection_new(MPD_HOST, MPD_PORT, 0); + if (mpd_connection_get_error(server->mpd_conn) != MPD_ERROR_SUCCESS) { + syslog(LOG_ERR, "Failed to connect to MPD: %s", + mpd_connection_get_error_message(server->mpd_conn)); + mpd_connection_free(server->mpd_conn); + server->mpd_conn = NULL; + return -1; + } + + server->mpd_idle_active = 0; + syslog(LOG_INFO, "Connected to MPD at %s:%d", MPD_HOST, MPD_PORT); + return 0; +} + +/* Disconnect from MPD */ +void mpd_ws_disconnect(struct mpd_ws_server *server) { + if (server->mpd_conn) { + if (server->mpd_idle_active) { + mpd_send_noidle(server->mpd_conn); + mpd_response_finish(server->mpd_conn); + } + mpd_connection_free(server->mpd_conn); + server->mpd_conn = NULL; + } + server->mpd_idle_active = 0; +} + +/* Check if MPD is connected */ +int mpd_ws_is_connected(struct mpd_ws_server *server) { + return server->mpd_conn && + mpd_connection_get_error(server->mpd_conn) == MPD_ERROR_SUCCESS; +} + +/* Update current song and broadcast */ +void mpd_ws_update_song(struct mpd_ws_server *server) { + if (!mpd_ws_is_connected(server)) { + return; + } + + mpd_command_list_begin(server->mpd_conn, false); + mpd_send_current_song(server->mpd_conn); + mpd_command_list_end(server->mpd_conn); + + struct mpd_song *song = mpd_recv_song(server->mpd_conn); + if (mpd_connection_get_error(server->mpd_conn) != MPD_ERROR_SUCCESS) { + syslog(LOG_ERR, "Failed to get current song: %s", + mpd_connection_get_error_message(server->mpd_conn)); + return; + } + + /* Create song message */ + if (song == NULL) { + snprintf(server->current_song, MAX_MESSAGE_SIZE, "Now Playing: No song"); + } else { + const char *artist = mpd_song_get_tag(song, MPD_TAG_ARTIST, 0); + const char *title = mpd_song_get_tag(song, MPD_TAG_TITLE, 0); + + if (artist && title) { + snprintf(server->current_song, MAX_MESSAGE_SIZE, "Now Playing: %s - %s", + artist, title); + } else if (title) { + snprintf(server->current_song, MAX_MESSAGE_SIZE, "Now Playing: %s", + title); + } else { + const char *uri = mpd_song_get_uri(song); + snprintf(server->current_song, MAX_MESSAGE_SIZE, "Now Playing: %s", + uri ? uri : "Unknown"); + } + } + + syslog(LOG_DEBUG, "Broadcasting: %s", server->current_song); + client_broadcast(server, server->current_song); + + if (song) { + mpd_song_free(song); + } + mpd_response_finish(server->mpd_conn); +} + +/* Start MPD idle mode */ +int mpd_ws_start_idle(struct mpd_ws_server *server) { + if (!mpd_ws_is_connected(server)) { + return -1; + } + + if (!mpd_send_idle_mask(server->mpd_conn, MPD_IDLE_PLAYER)) { + syslog(LOG_ERR, "Failed to send idle command: %s", + mpd_connection_get_error_message(server->mpd_conn)); + return -1; + } + + server->mpd_idle_active = 1; + return 0; +} + +/* Process MPD idle response */ +void mpd_ws_process_idle(struct mpd_ws_server *server) { + if (!mpd_ws_is_connected(server) || !server->mpd_idle_active) { + return; + } + + enum mpd_idle events = mpd_recv_idle(server->mpd_conn, false); + server->mpd_idle_active = 0; + + if (mpd_connection_get_error(server->mpd_conn) != MPD_ERROR_SUCCESS) { + syslog(LOG_WARNING, "MPD idle error: %s", + mpd_connection_get_error_message(server->mpd_conn)); + return; + } + + if (events & MPD_IDLE_PLAYER) { + mpd_ws_update_song(server); + } + + /* Restart idle mode */ + mpd_ws_start_idle(server); +} + +/* Add client */ +void client_add(struct mpd_ws_server *server, struct lws *wsi) { + struct client_session *client = malloc(sizeof(struct client_session)); + if (!client) { + syslog(LOG_ERR, "Failed to allocate client session"); + return; + } + + client->wsi = wsi; + client->next = server->clients; + server->clients = client; + + syslog(LOG_INFO, "Client connected"); +} + +/* Remove client */ +void client_remove(struct mpd_ws_server *server, struct lws *wsi) { + struct client_session **current = &server->clients; + + while (*current) { + if ((*current)->wsi == wsi) { + struct client_session *to_remove = *current; + *current = (*current)->next; + free(to_remove); + syslog(LOG_INFO, "Client disconnected"); + return; + } + current = &(*current)->next; + } +} + +/* Broadcast message to all clients */ +void client_broadcast(struct mpd_ws_server *server, const char *message) { + struct client_session *client = server->clients; + size_t len = strlen(message); + + while (client) { + unsigned char buf[LWS_PRE + MAX_MESSAGE_SIZE]; + memcpy(&buf[LWS_PRE], message, len); + + if (lws_write(client->wsi, &buf[LWS_PRE], len, LWS_WRITE_TEXT) < 0) { + syslog(LOG_WARNING, "Failed to write to websocket"); + } + client = client->next; + } +} |
