Merge pull request #80 from AlexanderPavlenko/master
[shell-fm.git] / source / main.c
blobadcbada10c26f0b8d6206d2a3763734a4d8d6368
1 /*
2 Copyright (C) 2006-2010 by Jonas Kramer
3 Published under the terms of the GNU General Public License (GPL).
4 */
6 #define _GNU_SOURCE
9 #include <stdio.h>
10 #include <stdlib.h>
11 #include <string.h>
12 #include <unistd.h>
13 #include <fcntl.h>
14 #include <signal.h>
15 #include <sys/wait.h>
16 #include <time.h>
17 #include <sys/types.h>
18 #include <sys/stat.h>
20 #include <dirent.h>
22 #include "hash.h"
23 #include "util.h"
24 #include "service.h"
25 #include "interface.h"
26 #include "settings.h"
27 #include "autoban.h"
28 #include "sckif.h"
29 #include "playlist.h"
30 #include "submit.h"
31 #include "readline.h"
32 #include "radio.h"
33 #include "select.h"
34 #include "history.h"
35 #include "strary.h"
36 #include "rest.h"
38 #include "globals.h"
40 #ifndef PATH_MAX
41 #define PATH_MAX 4096
42 #endif
44 #ifdef __NetBSD__
45 # ifndef WCONTINUED
46 # define WCONTINUED 0 /* not available on NetBSD */
47 # endif
48 # ifndef WIFCONTINUED
49 # define WIFCONTINUED(x) ((x) == 0x13) /* SIGCONT */
50 # endif
51 #endif
53 #if !defined(WCONTINUED) || !defined(WIFCONTINUED)
54 # undef WCONTINUED
55 # define WCONTINUED 0
56 # undef WIFCONTINUED
57 # define WIFCONTINUED(wstat) (0)
58 #endif
60 static void cleanup(void);
61 static void cleanup_term(void);
62 static void forcequit(int);
63 static void help(const char *, int);
64 static void playsig(int);
65 static void stopsig(int);
67 pid_t ppid = 0;
69 int main(int argc, char ** argv) {
70 int option, nerror = 0, background = 0, quiet = 0, have_socket = 0;
71 time_t pauselength = 0;
72 char * proxy;
73 opterr = 0;
75 /* Create directories. */
76 makercd();
78 /* Load settings from ~/.shell-fm/shell-fm.rc. */
79 settings(rcpath("shell-fm.rc"), !0);
81 /* Enable discovery by default if it is set in configuration. */
82 if(haskey(& rc, "discovery"))
83 enable(DISCOVERY);
85 /* Disable RTP if option "no-rtp" is set to something. */
86 if(haskey(& rc, "no-rtp"))
87 disable(RTP);
89 /* If "daemon" is set in the configuration, enable daemon mode by default. */
90 if(haskey(& rc, "daemon"))
91 background = !0;
93 /* If "quiet" is set in the configuration, enable quiet mode by default. */
94 if(haskey(& rc, "quiet"))
95 quiet = !0;
97 /* Get proxy environment variable. */
98 if((proxy = getenv("http_proxy")) != NULL)
99 set(& rc, "proxy", proxy);
102 /* Parse through command line options. */
103 while(-1 != (option = getopt(argc, argv, ":dbhqi:p:D:y:Y:")))
104 switch(option) {
105 case 'd': /* Daemonize. */
106 background = !background;
107 break;
109 case 'i': /* IP to bind network interface to. */
110 set(& rc, "bind", optarg);
111 break;
113 case 'p': /* Port to listen on. */
114 if(atoi(optarg))
115 set(& rc, "port", optarg);
116 else {
117 fputs("Invalid port.\n", stderr);
118 ++nerror;
120 break;
122 case 'b': /* Batch mode */
123 batch = !0;
124 break;
126 case 'D': /* Path to audio device file. */
127 set(& rc, "device", optarg);
128 break;
130 case 'y': /* Proxy address. */
131 set(& rc, "proxy", optarg);
132 break;
134 case 'Y': /* SOCKS proxy address. */
135 set(& rc, "socks-proxy", optarg);
136 break;
138 case 'q': /* Quiet mode. */
139 quiet = !quiet;
140 break;
142 case 'h': /* Print help text and exit. */
143 help(argv[0], 0);
144 break;
146 case ':':
147 fprintf(stderr, "Missing argument for option -%c.\n\n", optopt);
148 ++nerror;
149 break;
151 case '?':
152 default:
153 fprintf(stderr, "Unknown option -%c.\n", optopt);
154 ++nerror;
155 break;
158 /* The next argument, if present, is the lastfm:// URL we want to play. */
159 if(optind > 0 && optind < argc && argv[optind]) {
160 const char * station = argv[optind];
162 set(& rc, "default-radio", station);
166 if(nerror)
167 help(argv[0], EXIT_FAILURE);
169 #ifdef EXTERN_ONLY
170 /* Abort if EXTERN_ONLY is defined and no extern command is present */
171 if(!haskey(& rc, "extern")) {
172 fputs("Can't continue without extern command.\n", stderr);
173 exit(EXIT_FAILURE);
175 #else
176 #ifndef LIBAO
177 if(!haskey(& rc, "device"))
178 set(& rc, "device", "/dev/audio");
179 #endif
180 #endif
182 if(!background && !quiet) {
183 puts("Shell.FM v" PACKAGE_VERSION ", (C) 2006-2012 by Jonas Kramer");
184 puts("Published under the terms of the GNU General Public License (GPL).");
186 #ifndef TUXBOX
187 puts("\nPress ? for help.\n");
188 #else
189 puts("Compiled for the use with Shell.FM Wrapper.\n");
190 #endif
191 fflush(stdout);
195 /* Open a port so Shell.FM can be controlled over network. */
196 if(haskey(& rc, "bind")) {
197 int port = 54311;
199 if(haskey(& rc, "port"))
200 port = atoi(value(& rc, "port"));
202 if(tcpsock(value(& rc, "bind"), (unsigned short) port))
203 have_socket = !0;
207 /* Open a UNIX socket for local "remote" control. */
208 if(haskey(& rc, "unix") && unixsock(value(& rc, "unix")))
209 have_socket = !0;
212 /* We can't daemonize if there's no possibility left to control Shell.FM. */
213 if(background && !have_socket) {
214 fputs("Can't daemonize without control socket.\n", stderr);
215 exit(EXIT_FAILURE);
218 memset(& data, 0, sizeof(struct hash));
219 memset(& track, 0, sizeof(struct hash));
220 memset(& playlist, 0, sizeof(struct playlist));
222 /* Fork to background. */
223 if(background) {
224 int null;
225 pid_t pid = fork();
227 if(pid == -1) {
228 fputs("Failed to daemonize.\n", stderr);
229 exit(EXIT_FAILURE);
230 } else if(pid) {
231 exit(EXIT_SUCCESS);
234 enable(QUIET);
236 /* Detach from TTY */
237 setsid();
238 pid = fork();
240 if(pid > 0)
241 exit(EXIT_SUCCESS);
243 /* Close stdin out and err */
244 close(0);
245 close(1);
246 close(2);
248 /* Redirect stdin and out to /dev/null */
249 null = open("/dev/null", O_RDWR);
250 dup(null);
251 dup(null);
254 ppid = getpid();
256 atexit(cleanup);
257 load_queue();
259 /* Set up signal handlers for communication with the playback process. */
260 signal(SIGINT, forcequit);
262 /* SIGUSR2 from playfork means it detected an error. */
263 signal(SIGUSR2, playsig);
265 /* Catch SIGTSTP to set pausetime when user suspends us with ^Z. */
266 signal(SIGTSTP, stopsig);
268 /* Authenticate to the Last.FM server. */
269 create_session();
271 if(!background) {
272 struct input keyboard = { 0, KEYBOARD };
273 register_handle(keyboard);
274 canon(0);
275 atexit(cleanup_term);
279 /* Play default radio, if specified. */
280 if(haskey(& rc, "default-radio")) {
281 if(!strcmp(value(& rc, "default-radio"), "last")) {
282 char ** history = load_history(), * last = NULL, ** p;
284 for(p = history; * p != NULL; ++p) {
285 last = * p;
288 set(& rc, "default-radio", last);
289 purge(history);
292 station(value(& rc, "default-radio"));
295 else if(!background)
296 radioprompt("radio url> ");
298 /* The main loop. */
299 while(!0) {
300 pid_t child;
301 int status, playnext = 0;
303 /* Check if anything died (submissions fork or playback fork). */
304 while((child = waitpid(-1, & status, WNOHANG | WUNTRACED | WCONTINUED)) > 0) {
305 if(child == subfork)
306 subdead(WEXITSTATUS(status));
307 else if(child == playfork) {
308 if(WIFSTOPPED(status)) {
309 /* time(& pausetime); */
311 else {
312 if(WIFCONTINUED(status)) {
313 signal(SIGTSTP, stopsig);
314 if(pausetime != 0) {
315 pauselength += time(NULL) - pausetime;
318 else {
319 playnext = !0;
320 unlinknp();
322 if(delayquit) {
323 quit();
326 pausetime = 0;
332 Check if the playback process died. If so, submit the data
333 of the last played track. Then check if there are tracks left
334 in the playlist; if not, try to refresh the list and stop the
335 stream if there are no new tracks to fetch.
336 Also handle user stopping the stream here. We need to check for
337 playnext != 0 before handling enabled(STOPPED) anyway, otherwise
338 playfork would still be running.
340 if(playnext) {
341 playfork = 0;
343 if(enabled(RTP)) {
344 unsigned duration, played, minimum;
346 duration = atoi(value(& track, "duration"));
347 played = time(NULL) - change_time - pauselength;
349 /* Allow user to specify minimum playback length (min. 50%). */
350 if(haskey(& rc, "minimum")) {
351 unsigned percent = atoi(value(& rc, "minimum"));
352 if(percent < 50)
353 percent = 50;
354 minimum = duration * percent / 100;
356 else {
357 minimum = duration / 2;
360 if(duration >= 30 && (played >= 240 || played > minimum)) {
361 debug("adding track to scrobble queue\n");
362 enqueue(& track);
364 else {
365 debug("track too short (duration %d, played %d, minimum %d) - not scrobbling\n", duration, played, minimum);
369 submit();
371 /* Check if the user stopped the stream. */
372 if(enabled(STOPPED) || error) {
373 freelist(& playlist);
374 empty(& track);
376 if(error) {
377 fputs("Playback stopped with an error.\n", stderr);
378 error = 0;
381 disable(STOPPED);
382 disable(CHANGED);
384 continue;
387 shift(& playlist);
391 if(playnext || enabled(CHANGED)) {
392 if(nextstation != NULL) {
393 playnext = 0;
394 disable(CHANGED);
396 if(!station(nextstation))
397 enable(STOPPED);
399 free(nextstation);
400 nextstation = NULL;
403 if(!enabled(STOPPED) && !playlist.left) {
404 expand(& playlist);
405 if(!playlist.left) {
406 puts("No tracks left.");
407 playnext = 0;
408 disable(CHANGED);
409 continue;
413 if(!playfork) {
415 If there was a track played before, and there is a gap
416 configured, wait that many seconds before playing the next
417 track.
419 if(playnext && !enabled(INTERRUPTED) && haskey(& rc, "gap")) {
420 int gap = atoi(value(& rc, "gap"));
422 if(gap > 0)
423 sleep(gap);
426 disable(INTERRUPTED);
428 if(play(& playlist)) {
429 time(& change_time);
430 pauselength = 0;
432 set(& track, "stationURL", current_station);
434 /* Print what's currently played. (Ondrej Novy) */
435 if(!background) {
436 if(enabled(CHANGED) && playlist.left > 0) {
437 puts(meta("Receiving %s.", M_COLORED, & track));
438 disable(CHANGED);
441 if(haskey(& rc, "title-format"))
442 printf("%s\n", meta(value(& rc, "title-format"), M_COLORED, & track));
443 else
444 printf("%s\n", meta("Now playing \"%t\" by %a.", M_COLORED, & track));
447 if(enabled(RTP)) {
448 notify_now_playing(& track);
452 /* Write track data into a file. */
453 if(haskey(& rc, "np-file") && haskey(& rc, "np-file-format")) {
454 signed np;
455 const char
456 * file = value(& rc, "np-file"),
457 * fmt = value(& rc, "np-file-format");
459 unlink(file);
460 if(-1 != (np = open(file, O_WRONLY | O_CREAT, 0644))) {
461 const char * output = meta(fmt, 0, & track);
462 if(output)
463 write(np, output, strlen(output));
464 close(np);
469 if(haskey(& rc, "screen-format")) {
470 const char * term = getenv("TERM");
471 if(term != NULL && !strncmp(term, "screen", 6)) {
472 const char * output =
473 meta(value(& rc, "screen-format"), 0, & track);
474 printf("\x1Bk%s\x1B\\", output);
479 if(haskey(& rc, "term-format")) {
480 const char * output =
481 meta(value(& rc, "term-format"), 0, & track);
482 printf("\x1B]2;%s\a", output);
486 /* Run a command with our track data. */
487 if(haskey(& rc, "np-unescaped-cmd"))
488 run(meta(value(& rc, "np-unescaped-cmd"), 0, & track));
489 if(haskey(& rc, "np-cmd"))
490 run(meta(value(& rc, "np-cmd"), M_SHELLESC, & track));
491 } else
492 change_time = 0;
495 if(banned(value(& track, "creator"))) {
496 puts(meta("%a is banned.", M_COLORED, & track));
497 rate(RATING_BAN);
498 fflush(stdout);
502 playnext = 0;
504 if(playfork && change_time && haskey(& track, "duration") && !pausetime) {
505 unsigned duration;
506 signed remain;
507 char remstr[32];
509 duration = atoi(value(& track, "duration"));
511 remain = (change_time + duration) - time(NULL) + pauselength;
513 snprintf(remstr, sizeof(remstr), "%d", remain);
514 set(& track, "remain", remstr);
516 if(!background) {
517 printf(
518 "%s%c",
519 meta("%r", M_COLORED, & track),
520 // strdup(meta("%v", M_COLORED, & track)),
521 batch ? '\n' : '\r'
523 fflush(stdout);
527 handle_input(1000000);
530 return 0;
534 static void help(const char * argv0, int error_code) {
535 fprintf(stderr,
536 "Shell.FM v" PACKAGE_VERSION ", (C) 2006-2010 by Jonas Kramer\n"
537 "Published under the terms of the GNU General Public License (GPL).\n"
538 "\n"
539 "%s [options] [lastfm://url]\n"
540 "\n"
541 " -d daemon mode.\n"
542 " -q quiet mode.\n"
543 " -i address to listen on.\n"
544 " -p port to listen on.\n"
545 " -b batch mode.\n"
546 " -D device to play on.\n"
547 " -y proxy url to connect through.\n"
548 " -Y SOCKS proxy url to connect through.\n"
549 " -h this help.\n",
550 argv0
553 exit(error_code);
557 static void cleanup(void) {
558 rmsckif();
560 if(haskey(& rc, "unix") && getpid() == ppid)
561 unlink(value(& rc, "unix"));
563 empty(& data);
564 empty(& rc);
565 empty(& track);
567 freelist(& playlist);
569 if(current_station) {
570 free(current_station);
571 current_station = NULL;
574 if(subfork)
575 waitpid(subfork, NULL, 0);
577 dump_queue();
579 /* Clean cache. */
580 if(!access(rcpath("cache"), R_OK | W_OK | X_OK)) {
581 const char * cache = rcpath("cache");
582 DIR * directory = opendir(cache);
584 if(directory != NULL) {
585 time_t expiry = 24 * 60 * 60; /* Expiration after 24h. */
586 struct dirent * entry;
587 struct stat status;
588 char path[PATH_MAX];
590 if(haskey(& rc, "expiry"))
591 expiry = atoi(value(& rc, "expiry"));
593 while((entry = readdir(directory)) != NULL) {
594 snprintf(path, sizeof(path), "%s/%s", cache, entry->d_name);
596 if(!stat(path, & status)) {
597 if(status.st_mtime < (time(NULL) - expiry)) {
598 unlink(path);
603 closedir(directory);
607 if(playfork)
608 kill(playfork, SIGUSR1);
611 static void cleanup_term(void) {
612 if (getpid() == ppid) {
613 canon(1);
618 static void forcequit(int sig) {
619 if(sig == SIGINT) {
620 fputs("Try to press Q next time you want to quit.\n", stderr);
621 exit(EXIT_FAILURE);
626 static void playsig(int sig) {
627 if(sig == SIGUSR2) {
628 error = !0;
629 enable(INTERRUPTED);
634 static void stopsig(int sig) {
635 if(sig == SIGTSTP) {
636 time(& pausetime);
638 signal(SIGTSTP, SIG_DFL);
639 raise(SIGTSTP);