main rudis.c
  1#include <dirent.h>
  2#include <fcntl.h>
  3#include <signal.h>
  4#include <stdio.h>
  5#include <stdlib.h>
  6#include <string.h>
  7#include <sys/select.h>
  8#include <sys/socket.h>
  9#include <sys/stat.h>
 10#include <sys/un.h>
 11#include <taglib/tag_c.h>
 12#include <time.h>
 13#include <unistd.h>
 14
 15#include "miniaudio.h"
 16
 17#include "config.h"
 18
 19#define LENGTH(x) (sizeof(x) / sizeof(x[0]))
 20#define MAX(a, b) ((a) > (b) ? (a) : (b))
 21
 22enum { NONE, PLAYING, PAUSED };
 23
 24typedef struct {
 25  const char *name;
 26  const char *path;
 27  char **files;
 28  int count;
 29} Playlist;
 30
 31static Playlist pls[LENGTH(playlists)];
 32static ma_engine engine;
 33static ma_sound sound;
 34static int hs = 0;
 35static int state = NONE;
 36static int running = 1;
 37static int pli = -1;
 38static int tri = -1;
 39static int sigpipe[2];
 40
 41static int strcmp_sort(const void *a, const void *b) {
 42  return strcmp(*(const char **)a, *(const char **)b);
 43}
 44
 45#if notifications
 46static void notify(const char *msg) {
 47  char cmd[1024];
 48  snprintf(cmd, sizeof(cmd), "notify-send rudis \"%s\" -c 1984 -r 1984 -t %d",
 49           msg, notifications_timeout);
 50  system(cmd);
 51}
 52#else
 53#define notify(msg) ((void)0)
 54#endif
 55
 56static void scan(Playlist *pl) {
 57  DIR *dir = opendir(pl->path);
 58  if (!dir)
 59    return;
 60  struct dirent *entry;
 61  char **found = NULL;
 62  int count = 0;
 63  while ((entry = readdir(dir)) != NULL) {
 64    if (entry->d_type == DT_DIR) {
 65      if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
 66        continue;
 67      char sub[4096];
 68      snprintf(sub, sizeof(sub), "%s/%s", pl->path, entry->d_name);
 69      Playlist tmp = {.path = sub};
 70      scan(&tmp);
 71      for (int i = 0; i < tmp.count; i++) {
 72        found = realloc(found, (count + 1) * sizeof(char *));
 73        found[count++] = tmp.files[i];
 74      }
 75      free(tmp.files);
 76      continue;
 77    }
 78    char *ext = strrchr(entry->d_name, '.');
 79    if (!ext)
 80      continue;
 81    ext++;
 82    int ok = 0;
 83    for (int i = 0; i < LENGTH(extensions); i++) {
 84      if (strcasecmp(ext, extensions[i]) == 0) {
 85        ok = 1;
 86        break;
 87      }
 88    }
 89    if (!ok)
 90      continue;
 91    char full[4096];
 92    snprintf(full, sizeof(full), "%s/%s", pl->path, entry->d_name);
 93    found = realloc(found, (count + 1) * sizeof(char *));
 94    found[count++] = strdup(full);
 95  }
 96  closedir(dir);
 97  qsort(found, count, sizeof(char *), strcmp_sort);
 98  pl->files = found;
 99  pl->count = count;
100}
101
102static void save_scan(void) {
103  FILE *f = fopen(SCAN_FILE, "w");
104  if (!f)
105    return;
106  for (int i = 0; i < LENGTH(playlists); i++) {
107    if (!pls[i].files)
108      continue;
109    for (int j = 0; j < pls[i].count; j++) {
110      fprintf(f, "%s\n", pls[i].files[j]);
111    }
112  }
113  fclose(f);
114}
115
116static void shuf(Playlist *pl) {
117  if (!shuffle || pl->count < 2)
118    return;
119  for (int i = pl->count - 1; i > 0; i--) {
120    int j = rand() % (i + 1);
121    char *tmp = pl->files[i];
122    pl->files[i] = pl->files[j];
123    pl->files[j] = tmp;
124  }
125}
126
127static char *get_title(const char *path) {
128  TagLib_File *file = taglib_file_new(path);
129  if (!file || !taglib_file_is_valid(file)) {
130    if (file)
131      taglib_file_free(file);
132    const char *base = strrchr(path, '/');
133    if (!base)
134      return strdup(path);
135    base++;
136    char *t = strdup(base);
137    char *dot = strrchr(t, '.');
138    if (dot)
139      *dot = '\0';
140    return t;
141  }
142
143  TagLib_Tag *tag = taglib_file_tag(file);
144  char *title = strdup(taglib_tag_title(tag));
145  taglib_tag_free_strings();
146  taglib_file_free(file);
147
148  if (!title || strlen(title) == 0) {
149    free(title);
150    const char *base = strrchr(path, '/');
151    if (!base)
152      return strdup(path);
153    base++;
154    char *t = strdup(base);
155    char *dot = strrchr(t, '.');
156    if (dot)
157      *dot = '\0';
158    return t;
159  }
160
161  return title;
162}
163
164static void play_file(const char *path) {
165  if (hs) {
166    ma_sound_uninit(&sound);
167    hs = 0;
168  }
169  ma_result r = ma_sound_init_from_file(&engine, path, 0, NULL, NULL, &sound);
170  if (r == MA_SUCCESS) {
171    ma_sound_start(&sound);
172    hs = 1;
173    state = PLAYING;
174  }
175}
176
177static void toggle(void) {
178  if (state == NONE)
179    return;
180  if (state == PLAYING) {
181    ma_sound_stop(&sound);
182    state = PAUSED;
183  } else if (state == PAUSED) {
184    ma_sound_start(&sound);
185    state = PLAYING;
186  }
187}
188
189static void next(void) {
190  if (pli < 0 || tri < 0)
191    return;
192  if (hs) {
193    ma_sound_uninit(&sound);
194    hs = 0;
195  }
196  tri++;
197  if (tri >= pls[pli].count) {
198    if (loop) {
199      tri = 0;
200    } else {
201      state = NONE;
202      return;
203    }
204  }
205  play_file(pls[pli].files[tri]);
206}
207
208static void prev(void) {
209  if (pli < 0 || tri < 0)
210    return;
211  if (hs) {
212    ma_sound_uninit(&sound);
213    hs = 0;
214  }
215  tri--;
216  if (tri < 0) {
217    if (loop) {
218      tri = pls[pli].count - 1;
219    } else {
220      tri = 0;
221    }
222  }
223  play_file(pls[pli].files[tri]);
224}
225
226static void handle_sig(int sig) {
227  int s = sig;
228  write(sigpipe[1], &s, sizeof(int));
229}
230
231static void setup_signals(void) {
232  pipe(sigpipe);
233  fcntl(sigpipe[0], F_SETFL, O_NONBLOCK);
234  fcntl(sigpipe[1], F_SETFL, O_NONBLOCK);
235
236  struct sigaction sa;
237  sa.sa_handler = handle_sig;
238  sigemptyset(&sa.sa_mask);
239  sa.sa_flags = 0;
240
241  sigaction(SIGUSR1, &sa, NULL);
242  sigaction(SIGUSR2, &sa, NULL);
243  sigaction(SIGRTMIN, &sa, NULL);
244  sigaction(SIGINT, &sa, NULL);
245}
246
247static void process_signals(void) {
248  int sig;
249  while (read(sigpipe[0], &sig, sizeof(int)) > 0) {
250    if (sig == SIGUSR1) {
251      prev();
252      if (pli >= 0 && tri >= 0) {
253        char *t = get_title(pls[pli].files[tri]);
254        char buf[512];
255        snprintf(buf, sizeof(buf), "now playing: %s", t);
256        free(t);
257        notify(buf);
258      }
259    } else if (sig == SIGUSR2) {
260      next();
261      if (pli >= 0 && tri >= 0) {
262        char *t = get_title(pls[pli].files[tri]);
263        char buf[512];
264        snprintf(buf, sizeof(buf), "now playing: %s", t);
265        free(t);
266        notify(buf);
267      }
268    } else if (sig == 34) {
269      toggle();
270      notify(state == PLAYING ? "resumed" : "paused");
271    } else if (sig == SIGINT) {
272      running = 0;
273    }
274  }
275}
276
277static void handle_client(int fd) {
278  char cmd[256];
279  int n = read(fd, cmd, sizeof(cmd) - 1);
280  if (n <= 0) {
281    close(fd);
282    return;
283  }
284  cmd[n] = '\0';
285  char resp[4096] = "";
286
287  if (strcmp(cmd, "list") == 0) {
288    char *p = resp;
289    for (int i = 0; i < LENGTH(playlists); i++) {
290      p += sprintf(p, "[%d] %s\n", i, playlists[i].name);
291    }
292  } else if (strncmp(cmd, "cue ", 4) == 0) {
293    int idx = atoi(cmd + 4);
294    if (idx >= 0 && idx < LENGTH(playlists)) {
295      if (!pls[idx].files) {
296        pls[idx].name = playlists[idx].name;
297        pls[idx].path = playlists[idx].path;
298        scan(&pls[idx]);
299        shuf(&pls[idx]);
300        save_scan();
301      }
302      pli = idx;
303      tri = 0;
304      if (hs) {
305        ma_sound_uninit(&sound);
306        hs = 0;
307      }
308      state = NONE;
309      if (pls[pli].count > 0) {
310        play_file(pls[pli].files[tri]);
311        char *t = get_title(pls[pli].files[tri]);
312        snprintf(resp, sizeof(resp), "cued [%s]\n", playlists[idx].name);
313        char buf[512];
314        snprintf(buf, sizeof(buf), "now playing: %s", t);
315        free(t);
316        notify(buf);
317      }
318    }
319  } else if (strcmp(cmd, "play") == 0) {
320    if (pli >= 0 && pls[pli].count > 0) {
321      if (state == PAUSED) {
322        ma_sound_start(&sound);
323        state = PLAYING;
324        notify("resumed");
325      } else if (state != PLAYING) {
326        play_file(pls[pli].files[tri]);
327        char *t = get_title(pls[pli].files[tri]);
328        char buf[512];
329        snprintf(buf, sizeof(buf), "now playing: %s", t);
330        free(t);
331        notify(buf);
332      }
333    }
334  } else if (strcmp(cmd, "pause") == 0) {
335    if (state == PLAYING) {
336      ma_sound_stop(&sound);
337      state = PAUSED;
338      notify("paused");
339    }
340  } else if (strcmp(cmd, "toggle") == 0) {
341    toggle();
342    notify(state == PLAYING ? "resumed" : "paused");
343  } else if (strcmp(cmd, "next") == 0) {
344    next();
345    if (pli >= 0 && tri >= 0) {
346      char *t = get_title(pls[pli].files[tri]);
347      snprintf(resp, sizeof(resp), "now playing: %s\n", t);
348      notify(resp);
349      free(t);
350    }
351  } else if (strcmp(cmd, "previous") == 0) {
352    prev();
353    if (pli >= 0 && tri >= 0) {
354      char *t = get_title(pls[pli].files[tri]);
355      snprintf(resp, sizeof(resp), "now playing: %s\n", t);
356      notify(resp);
357      free(t);
358    }
359  } else if (strcmp(cmd, "status") == 0) {
360    float pos = 0, dur = 0;
361    if (hs) {
362      ma_sound_get_cursor_in_seconds(&sound, &pos);
363      ma_sound_get_length_in_seconds(&sound, &dur);
364    }
365    char *status_str = state == NONE      ? "idle"
366                       : state == PLAYING ? "playing"
367                                          : "paused";
368    const char *plname = pli >= 0 ? pls[pli].name : "none";
369    char track_buf[256] = "none";
370    if (hs && pli >= 0 && tri >= 0) {
371      char *t = get_title(pls[pli].files[tri]);
372      snprintf(track_buf, sizeof(track_buf), "%s", t);
373      free(t);
374    }
375    snprintf(resp, sizeof(resp), "%s: [%s] %s\n%d:%02d/%d:%02d\n", status_str,
376             plname, track_buf, (int)pos / 60, (int)pos % 60, (int)dur / 60,
377             (int)dur % 60);
378  }
379
380  write(fd, resp, strlen(resp));
381  close(fd);
382}
383
384static int client_mode(int argc, char **argv) {
385  int fd = socket(AF_UNIX, SOCK_STREAM, 0);
386  struct sockaddr_un addr = {.sun_family = AF_UNIX};
387  strcpy(addr.sun_path, SOCKET_PATH);
388  if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
389    fprintf(stderr, "rudis daemon is not running\n");
390    return 1;
391  }
392
393  char cmd[256] = "";
394  if (strcmp(argv[1], "list") == 0) {
395    strcpy(cmd, "list");
396  } else if (strcmp(argv[1], "cue") == 0 && argc > 2) {
397    snprintf(cmd, sizeof(cmd), "cue %s", argv[2]);
398  } else if (strcmp(argv[1], "play") == 0) {
399    strcpy(cmd, "play");
400  } else if (strcmp(argv[1], "pause") == 0) {
401    strcpy(cmd, "pause");
402  } else if (strcmp(argv[1], "toggle") == 0) {
403    strcpy(cmd, "toggle");
404  } else if (strcmp(argv[1], "next") == 0) {
405    strcpy(cmd, "next");
406  } else if (strcmp(argv[1], "previous") == 0) {
407    strcpy(cmd, "previous");
408  } else if (strcmp(argv[1], "status") == 0) {
409    strcpy(cmd, "status");
410  } else {
411    fprintf(stderr, "unknown command\n");
412    return 1;
413  }
414
415  write(fd, cmd, strlen(cmd));
416  char resp[4096];
417  int n = read(fd, resp, sizeof(resp) - 1);
418  if (n > 0) {
419    resp[n] = '\0';
420    printf("%s", resp);
421  }
422  close(fd);
423  return 0;
424}
425
426int main(int argc, char **argv) {
427  if (argc > 1 &&
428      (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) {
429    system("man rudis");
430    return 0;
431  }
432
433  if (argc > 1)
434    return client_mode(argc, argv);
435
436  setup_signals();
437  ma_engine_init(NULL, &engine);
438
439  int server = socket(AF_UNIX, SOCK_STREAM, 0);
440  struct sockaddr_un addr = {.sun_family = AF_UNIX};
441  strcpy(addr.sun_path, SOCKET_PATH);
442  unlink(SOCKET_PATH);
443  bind(server, (struct sockaddr *)&addr, sizeof(addr));
444  listen(server, 5);
445  srand(time(NULL));
446
447  sigset_t mask, oldmask;
448  sigemptyset(&mask);
449  sigaddset(&mask, SIGINT);
450  sigaddset(&mask, SIGUSR1);
451  sigaddset(&mask, SIGUSR2);
452  sigaddset(&mask, SIGRTMIN);
453  sigprocmask(SIG_BLOCK, &mask, &oldmask);
454
455  while (running) {
456    fd_set fds;
457    FD_ZERO(&fds);
458    FD_SET(server, &fds);
459    FD_SET(sigpipe[0], &fds);
460    int maxfd = MAX(server, sigpipe[0]);
461
462    struct timespec ts = {.tv_sec = 0, .tv_nsec = 100000000};
463    int ret = pselect(maxfd + 1, &fds, NULL, NULL, &ts, &oldmask);
464
465    if (ret < 0)
466      continue;
467
468    if (FD_ISSET(sigpipe[0], &fds))
469      process_signals();
470    if (FD_ISSET(server, &fds)) {
471      int client = accept(server, NULL, NULL);
472      handle_client(client);
473    }
474
475    if (hs && ma_sound_at_end(&sound)) {
476      ma_sound_uninit(&sound);
477      hs = 0;
478      next();
479      if (pli >= 0 && tri >= 0) {
480        char *t = get_title(pls[pli].files[tri]);
481        char buf[512];
482        snprintf(buf, sizeof(buf), "now playing: %s", t);
483        free(t);
484        notify(buf);
485      }
486    }
487  }
488
489  close(server);
490  unlink(SOCKET_PATH);
491  ma_engine_uninit(&engine);
492
493  for (int i = 0; i < LENGTH(playlists); i++) {
494    if (pls[i].files) {
495      for (int j = 0; j < pls[i].count; j++)
496        free(pls[i].files[j]);
497      free(pls[i].files);
498    }
499  }
500
501  return 0;
502}