1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
|
#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);
/* Only broadcast if song actually changed */
if (strcmp(server->current_song, server->previous_song) != 0) {
client_broadcast(server, server->current_song);
strlcpy(server->previous_song, server->current_song,
sizeof(server->previous_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;
}
}
|