#include "mpdws.h" #include #include #include #include #include #include #include #include static struct mpd_ws_server *g_server = NULL; static void signal_handler(int sig) { (void)sig; if (g_server) g_server->running = 0; } static void broadcast_current_song(struct mpd_ws_server *server) { struct client_session *client = server->clients; size_t len = strlen(server->current_song); while (client) { unsigned char buf[LWS_PRE + MAX_MESSAGE_SIZE]; memcpy(&buf[LWS_PRE], server->current_song, len); lws_write(client->wsi, &buf[LWS_PRE], len, LWS_WRITE_TEXT); client = client->next; } } static int ws_callback(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) { (void)user; (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: { struct client_session *client = malloc(sizeof(*client)); if (client) { client->wsi = wsi; client->next = server->clients; server->clients = client; syslog(LOG_INFO, "Client connected"); if (strlen(server->current_song) > 0) { lws_callback_on_writable(wsi); } } break; } case LWS_CALLBACK_CLOSED: { 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"); break; } current = &(*current)->next; } 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; } return 0; } static struct lws_protocols protocols[] = { {"mpd-protocol", ws_callback, 0, MAX_MESSAGE_SIZE, 0, NULL, 0}, {NULL, NULL, 0, 0, 0, NULL, 0}}; static int connect_mpd(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 = mpd_connection_new(MPD_HOST, MPD_PORT, 0); server->mpd_idle_active = 0; if (mpd_connection_get_error(server->mpd_conn) != MPD_ERROR_SUCCESS) { syslog(LOG_ERR, "MPD connection failed: %s", mpd_connection_get_error_message(server->mpd_conn)); mpd_connection_free(server->mpd_conn); server->mpd_conn = NULL; return -1; } syslog(LOG_INFO, "Connected to MPD at %s:%d", MPD_HOST, MPD_PORT); return 0; } static void update_current_song(struct mpd_ws_server *server) { if (!server->mpd_conn || mpd_connection_get_error(server->mpd_conn) != MPD_ERROR_SUCCESS) { return; } mpd_send_current_song(server->mpd_conn); struct mpd_song *song = mpd_recv_song(server->mpd_conn); if (mpd_connection_get_error(server->mpd_conn) != MPD_ERROR_SUCCESS) { mpd_response_finish(server->mpd_conn); return; } char new_song[MAX_MESSAGE_SIZE]; if (!song) { snprintf(new_song, sizeof(new_song), "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); snprintf(new_song, sizeof(new_song), "Now Playing: %s - %s", artist, title); mpd_song_free(song); } // Only send song if it's changed if (strcmp(new_song, server->current_song) != 0) { strlcpy(server->current_song, new_song, sizeof(server->current_song)); broadcast_current_song(server); syslog(LOG_DEBUG, "Broadcasting: %s", server->current_song); } mpd_response_finish(server->mpd_conn); } int mpd_ws_init(struct mpd_ws_server *server) { memset(server, 0, sizeof(*server)); g_server = server; signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); openlog("mpd_ws", LOG_PID | LOG_NDELAY, LOG_DAEMON); struct lws_context_creation_info info = {0}; info.port = WEBSOCKET_PORT; info.protocols = protocols; info.gid = 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_mpd(server); server->running = 1; return 0; } void mpd_ws_run(struct mpd_ws_server *server) { time_t last_reconnect = 0; while (server->running) { lws_service(server->ws_context, 10); // Handle MPD connection if (!server->mpd_conn || mpd_connection_get_error(server->mpd_conn) != MPD_ERROR_SUCCESS) { time_t now = time(NULL); if (now - last_reconnect >= RECONNECT_INTERVAL_SEC) { if (connect_mpd(server) == 0) { update_current_song(server); } last_reconnect = now; } sleep(SELECT_TIMEOUT_SEC); continue; } // Start idle if needed if (!server->mpd_idle_active) { if (mpd_send_idle_mask(server->mpd_conn, MPD_IDLE_PLAYER)) { server->mpd_idle_active = 1; } else { connect_mpd(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); if (select(mpd_fd + 1, &readfds, NULL, NULL, &timeout) > 0 && FD_ISSET(mpd_fd, &readfds)) { 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) { if (events & MPD_IDLE_PLAYER) { update_current_song(server); } } } } } void mpd_ws_cleanup(struct mpd_ws_server *server) { syslog(LOG_INFO, "Shutting down"); 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); } if (server->ws_context) { lws_context_destroy(server->ws_context); } while (server->clients) { struct client_session *next = server->clients->next; free(server->clients); server->clients = next; } closelog(); }