initial commit
This commit is contained in:
11
Makefile
Normal file
11
Makefile
Normal file
@@ -0,0 +1,11 @@
|
||||
.PHONY: all clean install uninstall
|
||||
|
||||
PROG= mpdws
|
||||
SRCS= mpd_ws.h mpd_ws.c main.c
|
||||
NOMAN=
|
||||
|
||||
CPPFLAGS+= -I/usr/local/include
|
||||
LDFLAGS+= -L/usr/local/lib
|
||||
LDADD+= -lmpdclient -lwebsockets
|
||||
|
||||
.include <bsd.prog.mk>
|
||||
4
README.md
Normal file
4
README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
This is the program which I use to display the "Now playing: ..." text on my radio page using `libwebsocketclient`. It uses `libmpdclient` to enter an idle state awaiting reported changes from the player. This way the program goes dormant until mpd informs it that the song has changed, then it will poll the current song info and send an update to each connected client.
|
||||
|
||||
The config variables are stored in `mpd_ws.h`.
|
||||
The Makefile is intended to build the binary using OpenBSD make(1) but it should work on every system.
|
||||
25
main.c
Normal file
25
main.c
Normal file
@@ -0,0 +1,25 @@
|
||||
#include "mpd_ws.h"
|
||||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
struct mpd_ws_server server;
|
||||
|
||||
printf("MPD WebSocket Bridge\n");
|
||||
printf("Connecting to MPD at %s:%d\n", MPD_HOST, MPD_PORT);
|
||||
printf("WebSocket server on port %d\n", WEBSOCKET_PORT);
|
||||
|
||||
/* Initialize server */
|
||||
if (mpd_ws_init(&server) < 0) {
|
||||
fprintf(stderr, "Failed to initialize server\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Run main loop */
|
||||
mpd_ws_run(&server);
|
||||
|
||||
/* Cleanup */
|
||||
mpd_ws_cleanup(&server);
|
||||
|
||||
printf("Server shut down\n");
|
||||
return 0;
|
||||
}
|
||||
346
mpd_ws.c
Normal file
346
mpd_ws.c
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
50
mpd_ws.h
Normal file
50
mpd_ws.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#ifndef MPD_WS_H
|
||||
#define MPD_WS_H
|
||||
|
||||
#include <libwebsockets.h>
|
||||
#include <mpd/client.h>
|
||||
|
||||
/* Configuration constants */
|
||||
#define MPD_HOST "localhost"
|
||||
#define MPD_PORT 6600
|
||||
#define WEBSOCKET_PORT 8765
|
||||
#define MAX_MESSAGE_SIZE 1024
|
||||
#define RECONNECT_INTERVAL_SEC 5
|
||||
#define SELECT_TIMEOUT_SEC 1
|
||||
|
||||
/* Client session structure */
|
||||
struct client_session {
|
||||
struct lws *wsi;
|
||||
struct client_session *next;
|
||||
};
|
||||
|
||||
/* Global state structure */
|
||||
struct mpd_ws_server {
|
||||
struct lws_context *ws_context;
|
||||
struct mpd_connection *mpd_conn;
|
||||
struct client_session *clients;
|
||||
char current_song[MAX_MESSAGE_SIZE];
|
||||
volatile int running;
|
||||
int mpd_idle_active;
|
||||
};
|
||||
|
||||
/* Function declarations */
|
||||
int mpd_ws_init(struct mpd_ws_server *server);
|
||||
void mpd_ws_run(struct mpd_ws_server *server);
|
||||
void mpd_ws_cleanup(struct mpd_ws_server *server);
|
||||
void mpd_ws_stop(struct mpd_ws_server *server);
|
||||
|
||||
/* MPD functions */
|
||||
int mpd_ws_connect(struct mpd_ws_server *server);
|
||||
void mpd_ws_disconnect(struct mpd_ws_server *server);
|
||||
int mpd_ws_is_connected(struct mpd_ws_server *server);
|
||||
void mpd_ws_update_song(struct mpd_ws_server *server);
|
||||
int mpd_ws_start_idle(struct mpd_ws_server *server);
|
||||
void mpd_ws_process_idle(struct mpd_ws_server *server);
|
||||
|
||||
/* Client management */
|
||||
void client_add(struct mpd_ws_server *server, struct lws *wsi);
|
||||
void client_remove(struct mpd_ws_server *server, struct lws *wsi);
|
||||
void client_broadcast(struct mpd_ws_server *server, const char *message);
|
||||
|
||||
#endif /* MPD_WS_H */
|
||||
Reference in New Issue
Block a user