#include "mpd_ws.h" #include #include #include #include #include #include #include #include /* 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); /* Only broadcast if song actually changed */ if (strcmp(server->current_song, server->previous_song) != 0) { client_broadcast(server, server->current_song); strcpy(server->previous_song, 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; } }