mpdws

websocket "now playing:" broadcast mpd hook
Download | Log | Files | Refs | README | LICENSE

mpdws.c (6728B)


      1 #include "mpdws.h"
      2 #include <signal.h>
      3 #include <stdio.h>
      4 #include <stdlib.h>
      5 #include <string.h>
      6 #include <sys/select.h>
      7 #include <syslog.h>
      8 #include <time.h>
      9 #include <unistd.h>
     10 
     11 static struct mpd_ws_server *g_server = NULL;
     12 
     13 static void signal_handler(int sig) {
     14   (void)sig;
     15   if (g_server)
     16     g_server->running = 0;
     17 }
     18 
     19 static void broadcast_current_song(struct mpd_ws_server *server) {
     20   struct client_session *client = server->clients;
     21   size_t len = strlen(server->current_song);
     22 
     23   while (client) {
     24     unsigned char buf[LWS_PRE + MAX_MESSAGE_SIZE];
     25     memcpy(&buf[LWS_PRE], server->current_song, len);
     26     lws_write(client->wsi, &buf[LWS_PRE], len, LWS_WRITE_TEXT);
     27     client = client->next;
     28   }
     29 }
     30 
     31 static int ws_callback(struct lws *wsi, enum lws_callback_reasons reason,
     32                        void *user, void *in, size_t len) {
     33   (void)user;
     34   (void)in;
     35   (void)len;
     36 
     37   struct mpd_ws_server *server =
     38       (struct mpd_ws_server *)lws_context_user(lws_get_context(wsi));
     39 
     40   switch (reason) {
     41   case LWS_CALLBACK_ESTABLISHED: {
     42     struct client_session *client = malloc(sizeof(*client));
     43     if (client) {
     44       client->wsi = wsi;
     45       client->next = server->clients;
     46       server->clients = client;
     47       syslog(LOG_INFO, "Client connected");
     48       if (strlen(server->current_song) > 0) {
     49         lws_callback_on_writable(wsi);
     50       }
     51     }
     52     break;
     53   }
     54   case LWS_CALLBACK_CLOSED: {
     55     struct client_session **current = &server->clients;
     56     while (*current) {
     57       if ((*current)->wsi == wsi) {
     58         struct client_session *to_remove = *current;
     59         *current = (*current)->next;
     60         free(to_remove);
     61         syslog(LOG_INFO, "Client disconnected");
     62         break;
     63       }
     64       current = &(*current)->next;
     65     }
     66     break;
     67   }
     68   case LWS_CALLBACK_SERVER_WRITEABLE:
     69     if (strlen(server->current_song) > 0) {
     70       unsigned char buf[LWS_PRE + MAX_MESSAGE_SIZE];
     71       size_t msg_len = strlen(server->current_song);
     72       memcpy(&buf[LWS_PRE], server->current_song, msg_len);
     73       lws_write(wsi, &buf[LWS_PRE], msg_len, LWS_WRITE_TEXT);
     74     }
     75     break;
     76   default:
     77     break;
     78   }
     79   return 0;
     80 }
     81 
     82 static struct lws_protocols protocols[] = {
     83     {"mpd-protocol", ws_callback, 0, MAX_MESSAGE_SIZE, 0, NULL, 0},
     84     {NULL, NULL, 0, 0, 0, NULL, 0}};
     85 
     86 static int connect_mpd(struct mpd_ws_server *server) {
     87   if (server->mpd_conn) {
     88     if (server->mpd_idle_active) {
     89       mpd_send_noidle(server->mpd_conn);
     90       mpd_response_finish(server->mpd_conn);
     91     }
     92     mpd_connection_free(server->mpd_conn);
     93   }
     94 
     95   server->mpd_conn = mpd_connection_new(MPD_HOST, MPD_PORT, 0);
     96   server->mpd_idle_active = 0;
     97 
     98   if (mpd_connection_get_error(server->mpd_conn) != MPD_ERROR_SUCCESS) {
     99     syslog(LOG_ERR, "MPD connection failed: %s",
    100            mpd_connection_get_error_message(server->mpd_conn));
    101     mpd_connection_free(server->mpd_conn);
    102     server->mpd_conn = NULL;
    103     return -1;
    104   }
    105 
    106   syslog(LOG_INFO, "Connected to MPD at %s:%d", MPD_HOST, MPD_PORT);
    107   return 0;
    108 }
    109 
    110 static void update_current_song(struct mpd_ws_server *server) {
    111   if (!server->mpd_conn ||
    112       mpd_connection_get_error(server->mpd_conn) != MPD_ERROR_SUCCESS) {
    113     return;
    114   }
    115 
    116   mpd_send_current_song(server->mpd_conn);
    117   struct mpd_song *song = mpd_recv_song(server->mpd_conn);
    118 
    119   if (mpd_connection_get_error(server->mpd_conn) != MPD_ERROR_SUCCESS) {
    120     mpd_response_finish(server->mpd_conn);
    121     return;
    122   }
    123 
    124   char new_song[MAX_MESSAGE_SIZE];
    125   if (!song) {
    126     snprintf(new_song, sizeof(new_song), "Now Playing: No song");
    127   } else {
    128     const char *artist = mpd_song_get_tag(song, MPD_TAG_ARTIST, 0);
    129     const char *title = mpd_song_get_tag(song, MPD_TAG_TITLE, 0);
    130 
    131     snprintf(new_song, sizeof(new_song), "Now Playing: %s - %s", artist, title);
    132     mpd_song_free(song);
    133   }
    134 
    135   // Only send song if it's changed
    136   if (strcmp(new_song, server->current_song) != 0) {
    137     strlcpy(server->current_song, new_song, sizeof(server->current_song));
    138     broadcast_current_song(server);
    139     syslog(LOG_DEBUG, "Broadcasting: %s", server->current_song);
    140   }
    141 
    142   mpd_response_finish(server->mpd_conn);
    143 }
    144 
    145 int mpd_ws_init(struct mpd_ws_server *server) {
    146   memset(server, 0, sizeof(*server));
    147 
    148   g_server = server;
    149   signal(SIGINT, signal_handler);
    150   signal(SIGTERM, signal_handler);
    151   openlog("mpd_ws", LOG_PID | LOG_NDELAY, LOG_DAEMON);
    152 
    153   struct lws_context_creation_info info = {0};
    154   info.port = WEBSOCKET_PORT;
    155   info.protocols = protocols;
    156   info.gid = info.uid = -1;
    157   info.user = server;
    158 
    159   server->ws_context = lws_create_context(&info);
    160   if (!server->ws_context) {
    161     syslog(LOG_ERR, "Failed to create WebSocket context");
    162     return -1;
    163   }
    164 
    165   syslog(LOG_INFO, "WebSocket server listening on port %d", WEBSOCKET_PORT);
    166   connect_mpd(server);
    167   server->running = 1;
    168   return 0;
    169 }
    170 
    171 void mpd_ws_run(struct mpd_ws_server *server) {
    172   time_t last_reconnect = 0;
    173 
    174   while (server->running) {
    175     lws_service(server->ws_context, 10);
    176 
    177     // Handle MPD connection
    178     if (!server->mpd_conn ||
    179         mpd_connection_get_error(server->mpd_conn) != MPD_ERROR_SUCCESS) {
    180       time_t now = time(NULL);
    181       if (now - last_reconnect >= RECONNECT_INTERVAL_SEC) {
    182         if (connect_mpd(server) == 0) {
    183           update_current_song(server);
    184         }
    185         last_reconnect = now;
    186       }
    187       sleep(SELECT_TIMEOUT_SEC);
    188       continue;
    189     }
    190 
    191     // Start idle if needed
    192     if (!server->mpd_idle_active) {
    193       if (mpd_send_idle_mask(server->mpd_conn, MPD_IDLE_PLAYER)) {
    194         server->mpd_idle_active = 1;
    195       } else {
    196         connect_mpd(server);
    197         continue;
    198       }
    199     }
    200 
    201     // Check for MPD events
    202     fd_set readfds;
    203     int mpd_fd = mpd_connection_get_fd(server->mpd_conn);
    204     struct timeval timeout = {0, SELECT_TIMEOUT_SEC};
    205 
    206     FD_ZERO(&readfds);
    207     FD_SET(mpd_fd, &readfds);
    208 
    209     if (select(mpd_fd + 1, &readfds, NULL, NULL, &timeout) > 0 &&
    210         FD_ISSET(mpd_fd, &readfds)) {
    211 
    212       enum mpd_idle events = mpd_recv_idle(server->mpd_conn, false);
    213       server->mpd_idle_active = 0;
    214 
    215       if (mpd_connection_get_error(server->mpd_conn) == MPD_ERROR_SUCCESS) {
    216         if (events & MPD_IDLE_PLAYER) {
    217           update_current_song(server);
    218         }
    219       }
    220     }
    221   }
    222 }
    223 
    224 void mpd_ws_cleanup(struct mpd_ws_server *server) {
    225   syslog(LOG_INFO, "Shutting down");
    226 
    227   if (server->mpd_conn) {
    228     if (server->mpd_idle_active) {
    229       mpd_send_noidle(server->mpd_conn);
    230       mpd_response_finish(server->mpd_conn);
    231     }
    232     mpd_connection_free(server->mpd_conn);
    233   }
    234 
    235   if (server->ws_context) {
    236     lws_context_destroy(server->ws_context);
    237   }
    238 
    239   while (server->clients) {
    240     struct client_session *next = server->clients->next;
    241     free(server->clients);
    242     server->clients = next;
    243   }
    244 
    245   closelog();
    246 }