Updated year to 2010 in copyright notes.
[shell-fm.git] / source / main.c
blob0ed37dd46bd42feefa024e7af85b860d9467835b
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 "service.h"
24 #include "interface.h"
25 #include "settings.h"
26 #include "autoban.h"
27 #include "sckif.h"
28 #include "playlist.h"
29 #include "submit.h"
30 #include "readline.h"
31 #include "radio.h"
33 #include "globals.h"
35 #ifndef PATH_MAX
36 #define PATH_MAX 4096
37 #endif
39 #ifdef __NetBSD__
40 # ifndef WCONTINUED
41 # define WCONTINUED 0 /* not available on NetBSD */
42 # endif
43 # ifndef WIFCONTINUED
44 # define WIFCONTINUED(x) ((x) == 0x13) /* SIGCONT */
45 # endif
46 #endif
48 #if !defined(WCONTINUED) || !defined(WIFCONTINUED)
49 # undef WCONTINUED
50 # define WCONTINUED 0
51 # undef WIFCONTINUED
52 # define WIFCONTINUED(wstat) (0)
53 #endif
55 unsigned flags = RTP;
56 time_t changeTime = 0, pausetime = 0;
57 char * nextstation = NULL;
59 int batch = 0, error = 0, delayquit = 0;
61 static void cleanup(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, haveSocket = 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 /* Get proxy environment variable. */
94 if((proxy = getenv("http_proxy")) != NULL)
95 set(& rc, "proxy", proxy);
98 /* Parse through command line options. */
99 while(-1 != (option = getopt(argc, argv, ":dbhi:p:D:y:")))
100 switch(option) {
101 case 'd': /* Daemonize. */
102 background = !background;
103 break;
105 case 'i': /* IP to bind network interface to. */
106 set(& rc, "bind", optarg);
107 break;
109 case 'p': /* Port to listen on. */
110 if(atoi(optarg))
111 set(& rc, "port", optarg);
112 else {
113 fputs("Invalid port.\n", stderr);
114 ++nerror;
116 break;
118 case 'b': /* Batch mode */
119 batch = !0;
120 break;
122 case 'D': /* Path to audio device file. */
123 set(& rc, "device", optarg);
124 break;
126 case 'y': /* Proxy address. */
127 set(& rc, "proxy", optarg);
128 break;
130 case 'h': /* Print help text and exit. */
131 help(argv[0], 0);
132 break;
134 case ':':
135 fprintf(stderr, "Missing argument for option -%c.\n\n", optopt);
136 ++nerror;
137 break;
139 case '?':
140 default:
141 fprintf(stderr, "Unknown option -%c.\n", optopt);
142 ++nerror;
143 break;
146 /* The next argument, if present, is the lastfm:// URL we want to play. */
147 if(optind > 0 && optind < argc && argv[optind]) {
148 const char * station = argv[optind];
150 set(& rc, "default-radio", station);
154 if(nerror)
155 help(argv[0], EXIT_FAILURE);
157 #ifdef EXTERN_ONLY
158 /* Abort if EXTERN_ONLY is defined and no extern command is present */
159 if(!haskey(& rc, "extern")) {
160 fputs("Can't continue without extern command.\n", stderr);
161 exit(EXIT_FAILURE);
163 #else
164 #ifndef LIBAO
165 if(!haskey(& rc, "device"))
166 set(& rc, "device", "/dev/audio");
167 #endif
168 #endif
170 if(!background) {
171 puts("Shell.FM v" PACKAGE_VERSION ", (C) 2006-2010 by Jonas Kramer");
172 puts("Published under the terms of the GNU General Public License (GPL).");
174 #ifndef TUXBOX
175 puts("\nPress ? for help.\n");
176 #else
177 puts("Compiled for the use with Shell.FM Wrapper.\n");
178 #endif
179 fflush(stdout);
183 /* Open a port so Shell.FM can be controlled over network. */
184 if(haskey(& rc, "bind")) {
185 int port = 54311;
187 if(haskey(& rc, "port"))
188 port = atoi(value(& rc, "port"));
190 if(tcpsock(value(& rc, "bind"), (unsigned short) port))
191 haveSocket = !0;
195 /* Open a UNIX socket for local "remote" control. */
196 if(haskey(& rc, "unix") && unixsock(value(& rc, "unix")))
197 haveSocket = !0;
200 /* We can't daemonize if there's no possibility left to control Shell.FM. */
201 if(background && !haveSocket) {
202 fputs("Can't daemonize without control socket.\n", stderr);
203 exit(EXIT_FAILURE);
207 /* Ask for username/password if they weren't specified in the .rc file. */
208 if(!haskey(& rc, "password")) {
209 char * password;
211 if(!haskey(& rc, "username")) {
212 char username[256] = { 0 };
214 struct prompt prompt = {
215 .prompt = "Login: ",
216 .line = getenv("USER"), .history = NULL, .callback = NULL,
219 strncpy(username, readline(& prompt), 255);
221 set(& rc, "username", username);
224 if(!(password = getpass("Password: ")))
225 exit(EXIT_FAILURE);
227 set(& rc, "password", password);
231 memset(& data, 0, sizeof(struct hash));
232 memset(& track, 0, sizeof(struct hash));
233 memset(& playlist, 0, sizeof(struct playlist));
235 /* Fork to background. */
236 if(background) {
237 int null;
238 pid_t pid = fork();
240 if(pid == -1) {
241 fputs("Failed to daemonize.\n", stderr);
242 exit(EXIT_FAILURE);
243 } else if(pid) {
244 exit(EXIT_SUCCESS);
247 enable(QUIET);
249 /* Detach from TTY */
250 setsid();
251 pid = fork();
253 if(pid > 0)
254 exit(EXIT_SUCCESS);
256 /* Close stdin out and err */
257 close(0);
258 close(1);
259 close(2);
261 /* Redirect stdin and out to /dev/null */
262 null = open("/dev/null", O_RDWR);
263 dup(null);
264 dup(null);
267 ppid = getpid();
269 atexit(cleanup);
270 loadqueue(!0);
272 /* Set up signal handlers for communication with the playback process. */
273 signal(SIGINT, forcequit);
275 /* SIGUSR2 from playfork means it detected an error. */
276 signal(SIGUSR2, playsig);
278 /* Catch SIGTSTP to set pausetime when user suspends us with ^Z. */
279 signal(SIGTSTP, stopsig);
282 /* Authenticate to the Last.FM server. */
283 if(!authenticate(value(& rc, "username"), value(& rc, "password")))
284 exit(EXIT_FAILURE);
286 /* Store session key for use by external tools. */
287 if(haskey(& data, "session")) {
288 FILE * fd = fopen(rcpath("session"), "w");
289 if(fd) {
290 fprintf(fd, "%s\n", value(& data, "session"));
291 fclose(fd);
296 /* Play default radio, if specified. */
297 if(haskey(& rc, "default-radio"))
298 station(value(& rc, "default-radio"));
299 else if(!background)
300 radioprompt("radio url> ");
302 /* The main loop. */
303 while(!0) {
304 pid_t child;
305 int status, playnext = 0;
307 /* Check if anything died (submissions fork or playback fork). */
308 while((child = waitpid(-1, & status, WNOHANG | WUNTRACED | WCONTINUED)) > 0) {
309 if(child == subfork)
310 subdead(WEXITSTATUS(status));
311 else if(child == playfork) {
312 if(WIFSTOPPED(status)) {
313 /* time(& pausetime); */
315 else {
316 if(WIFCONTINUED(status)) {
317 signal(SIGTSTP, stopsig);
318 if(pausetime != 0) {
319 pauselength += time(NULL) - pausetime;
322 else {
323 playnext = !0;
324 unlinknp();
326 if(delayquit) {
327 quit();
330 pausetime = 0;
336 Check if the playback process died. If so, submit the data
337 of the last played track. Then check if there are tracks left
338 in the playlist; if not, try to refresh the list and stop the
339 stream if there are no new tracks to fetch.
340 Also handle user stopping the stream here. We need to check for
341 playnext != 0 before handling enabled(STOPPED) anyway, otherwise
342 playfork would still be running.
344 if(playnext) {
345 playfork = 0;
347 if(enabled(RTP)) {
348 unsigned duration, played, minimum;
350 duration = atoi(value(& track, "duration"));
351 played = time(NULL) - changeTime - pauselength;
353 /* Allow user to specify minimum playback length (min. 50%). */
354 if(haskey(& rc, "minimum")) {
355 unsigned percent = atoi(value(& rc, "minimum"));
356 if(percent < 50)
357 percent = 50;
358 minimum = duration * percent / 100;
360 else {
361 minimum = duration / 2;
364 if(duration >= 30 && (played >= 240 || played > minimum))
365 enqueue(& track);
368 submit(value(& rc, "username"), value(& rc, "password"));
370 /* Check if the user stopped the stream. */
371 if(enabled(STOPPED) || error) {
372 freelist(& playlist);
373 empty(& track);
375 if(error) {
376 fputs("Playback stopped with an error.\n", stderr);
377 error = 0;
380 disable(STOPPED);
381 disable(CHANGED);
383 continue;
386 shift(& playlist);
390 if(playnext || enabled(CHANGED)) {
391 if(nextstation != NULL) {
392 playnext = 0;
393 disable(CHANGED);
395 if(!station(nextstation))
396 enable(STOPPED);
398 free(nextstation);
399 nextstation = NULL;
402 if(!enabled(STOPPED) && !playlist.left) {
403 expand(& playlist);
404 if(!playlist.left) {
405 puts("No tracks left.");
406 playnext = 0;
407 disable(CHANGED);
408 continue;
412 if(!playfork) {
414 If there was a track played before, and there is a gap
415 configured, wait that many seconds before playing the next
416 track.
418 if(playnext && !enabled(INTERRUPTED) && haskey(& rc, "gap")) {
419 int gap = atoi(value(& rc, "gap"));
421 if(gap > 0)
422 sleep(gap);
425 disable(INTERRUPTED);
427 if(play(& playlist)) {
428 time(& changeTime);
429 pauselength = 0;
431 set(& track, "stationURL", currentStation);
433 /* Print what's currently played. (Ondrej Novy) */
434 if(!background) {
435 if(enabled(CHANGED) && playlist.left > 0) {
436 puts(meta("Receiving %s.", M_COLORED, & track));
437 disable(CHANGED);
440 if(haskey(& rc, "title-format"))
441 printf("%s\n", meta(value(& rc, "title-format"), M_COLORED, & track));
442 else
443 printf("%s\n", meta("Now playing \"%t\" by %a.", M_COLORED, & track));
447 /* Write track data into a file. */
448 if(haskey(& rc, "np-file") && haskey(& rc, "np-file-format")) {
449 signed np;
450 const char
451 * file = value(& rc, "np-file"),
452 * fmt = value(& rc, "np-file-format");
454 unlink(file);
455 if(-1 != (np = open(file, O_WRONLY | O_CREAT, 0644))) {
456 const char * output = meta(fmt, 0, & track);
457 if(output)
458 write(np, output, strlen(output));
459 close(np);
464 if(haskey(& rc, "screen-format")) {
465 const char * term = getenv("TERM");
466 if(term != NULL && !strncmp(term, "screen", 6)) {
467 const char * output =
468 meta(value(& rc, "screen-format"), 0, & track);
469 printf("\x1Bk%s\x1B\\", output);
474 if(haskey(& rc, "term-format")) {
475 const char * output =
476 meta(value(& rc, "term-format"), 0, & track);
477 printf("\x1B]2;%s\a", output);
481 /* Run a command with our track data. */
482 if(haskey(& rc, "np-cmd"))
483 run(meta(value(& rc, "np-cmd"), M_SHELLESC, & track));
484 } else
485 changeTime = 0;
488 if(banned(value(& track, "creator"))) {
489 puts(meta("%a is banned.", M_COLORED, & track));
490 rate("B");
491 fflush(stdout);
495 playnext = 0;
497 if(playfork && changeTime && haskey(& track, "duration") && !pausetime) {
498 unsigned duration;
499 signed remain;
500 char remstr[32];
502 duration = atoi(value(& track, "duration"));
504 remain = (changeTime + duration) - time(NULL) + pauselength;
506 snprintf(remstr, sizeof(remstr), "%d", remain);
507 set(& track, "remain", remstr);
509 if(!background) {
510 printf(
511 "%c%02d:%02d%c",
512 remain < 0 ? '-' : ' ',
513 (remain >= 0) ? (remain / 60) : (-remain / 60),
514 (remain >= 0) ? (remain % 60) : (-remain % 60),
515 batch ? '\n' : '\r'
517 fflush(stdout);
521 interface(!background);
522 if(haveSocket)
523 sckif(background ? 2 : 0, -1);
526 return 0;
530 static void help(const char * argv0, int errorCode) {
531 fprintf(stderr,
532 "Shell.FM v" PACKAGE_VERSION ", (C) 2006-2010 by Jonas Kramer\n"
533 "Published under the terms of the GNU General Public License (GPL).\n"
534 "\n"
535 "%s [options] [lastfm://url]\n"
536 "\n"
537 " -d daemon mode.\n"
538 " -i address to listen on.\n"
539 " -p port to listen on.\n"
540 " -b batch mode.\n"
541 " -D device to play on.\n"
542 " -y proxy url to connect through.\n"
543 " -h this help.\n",
544 argv0
547 exit(errorCode);
551 static void cleanup(void) {
552 canon(!0);
553 rmsckif();
555 if(haskey(& rc, "unix") && getpid() == ppid)
556 unlink(value(& rc, "unix"));
558 empty(& data);
559 empty(& rc);
560 empty(& track);
562 freelist(& playlist);
564 if(currentStation) {
565 free(currentStation);
566 currentStation = NULL;
569 if(subfork)
570 waitpid(subfork, NULL, 0);
572 dumpqueue(!0);
574 /* Clean cache. */
575 if(!access(rcpath("cache"), R_OK | W_OK | X_OK)) {
576 const char * cache = rcpath("cache");
577 DIR * directory = opendir(cache);
579 if(directory != NULL) {
580 time_t expiry = 24 * 60 * 60; /* Expiration after 24h. */
581 struct dirent * entry;
582 struct stat status;
583 char path[PATH_MAX];
585 if(haskey(& rc, "expiry"))
586 expiry = atoi(value(& rc, "expiry"));
588 while((entry = readdir(directory)) != NULL) {
589 snprintf(path, sizeof(path), "%s/%s", cache, entry->d_name);
591 if(!stat(path, & status)) {
592 if(status.st_mtime < (time(NULL) - expiry)) {
593 unlink(path);
598 closedir(directory);
602 if(playfork)
603 kill(playfork, SIGUSR1);
607 static void forcequit(int sig) {
608 if(sig == SIGINT) {
609 fputs("Try to press Q next time you want to quit.\n", stderr);
610 exit(EXIT_FAILURE);
615 static void playsig(int sig) {
616 if(sig == SIGUSR2) {
617 error = !0;
618 enable(INTERRUPTED);
623 static void stopsig(int sig) {
624 if(sig == SIGTSTP) {
625 time(& pausetime);
627 signal(SIGTSTP, SIG_DFL);
628 raise(SIGTSTP);