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}