Added new download option to the manual.
[shell-fm.git] / source / main.c
blobf88fd370db8ed1add411e13048f38e1ff1388275
1 /*
2 Copyright (C) 2006-2008 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"
32 #include "globals.h"
34 #ifndef PATH_MAX
35 #define PATH_MAX 4096
36 #endif
38 #ifdef __NetBSD__
39 # ifndef WCONTINUED
40 # define WCONTINUED 0 /* not available on NetBSD */
41 # endif
42 # ifndef WIFCONTINUED
43 # define WIFCONTINUED(x) ((x) == 0x13) /* SIGCONT */
44 # endif
45 #endif
47 unsigned flags = RTP;
48 time_t changeTime = 0, pausetime = 0;
49 char * nextstation = NULL;
51 int batch = 0, error = 0;
53 static void cleanup(void);
54 static void forcequit(int);
55 static void help(const char *, int);
56 static void playsig(int);
57 static void stopsig(int);
58 static void unlinknp(void);
60 pid_t ppid = 0;
62 int main(int argc, char ** argv) {
63 int option, nerror = 0, background = 0, haveSocket = 0;
64 time_t pauselength = 0;
65 char * proxy;
66 opterr = 0;
68 /* Create directories. */
69 makercd();
71 /* Load settings from ~/.shell-fm/shell-fm.rc. */
72 settings(rcpath("shell-fm.rc"), !0);
74 /* Enable discovery by default if it is set in configuration. */
75 if(haskey(& rc, "discovery"))
76 enable(DISCOVERY);
78 /* Get proxy environment variable. */
79 if((proxy = getenv("http_proxy")) != NULL)
80 set(& rc, "proxy", proxy);
83 /* Parse through command line options. */
84 while(-1 != (option = getopt(argc, argv, ":dbhi:p:D:y:")))
85 switch(option) {
86 case 'd': /* Daemonize. */
87 background = !0;
88 break;
90 case 'i': /* IP to bind network interface to. */
91 set(& rc, "bind", optarg);
92 break;
94 case 'p': /* Port to listen on. */
95 if(atoi(optarg))
96 set(& rc, "port", optarg);
97 else {
98 fputs("Invalid port.\n", stderr);
99 ++nerror;
101 break;
103 case 'b': /* Batch mode */
104 batch = !0;
105 break;
107 case 'D': /* Path to audio device file. */
108 set(& rc, "device", optarg);
109 break;
111 case 'y': /* Proxy address. */
112 set(& rc, "proxy", optarg);
113 break;
115 case 'h': /* Print help text and exit. */
116 help(argv[0], 0);
117 break;
119 case ':':
120 fprintf(stderr, "Missing argument for option -%c.\n\n", optopt);
121 ++nerror;
122 break;
124 case '?':
125 default:
126 fprintf(stderr, "Unknown option -%c.\n", optopt);
127 ++nerror;
128 break;
131 /* The next argument, if present, is the lastfm:// URL we want to play. */
132 if(optind > 0 && optind < argc && argv[optind]) {
133 const char * station = argv[optind];
135 if(0 != strncmp(station, "lastfm://", 9)) {
136 fprintf(stderr, "Not a valid lastfm url: %s\n\n", station);
137 ++nerror;
138 } else {
139 set(& rc, "default-radio", station);
144 if(nerror)
145 help(argv[0], EXIT_FAILURE);
147 #ifndef LIBAO
148 if(!haskey(& rc, "device"))
149 set(& rc, "device", "/dev/audio");
150 #endif
152 if(!background) {
153 puts("Shell.FM v" PACKAGE_VERSION ", (C) 2006-2008 by Jonas Kramer");
154 puts("Published under the terms of the GNU General Public License (GPL).");
156 puts("\nPress ? for help.\n");
160 /* Open a port so Shell.FM can be controlled over network. */
161 if(haskey(& rc, "bind")) {
162 int port = 54311;
164 if(haskey(& rc, "port"))
165 port = atoi(value(& rc, "port"));
167 if(tcpsock(value(& rc, "bind"), (unsigned short) port))
168 haveSocket = !0;
172 /* Open a UNIX socket for local "remote" control. */
173 if(haskey(& rc, "unix") && unixsock(value(& rc, "unix")))
174 haveSocket = !0;
177 /* We can't daemonize if there's no possibility left to control Shell.FM. */
178 if(background && !haveSocket) {
179 fputs("Can't daemonize without control socket.\n", stderr);
180 exit(EXIT_FAILURE);
184 /* Ask for username/password if they weren't specified in the .rc file. */
185 if(!haskey(& rc, "password")) {
186 char * password;
188 if(!haskey(& rc, "username")) {
189 char username[256] = { 0 };
191 struct prompt prompt = {
192 .prompt = "Login: ",
193 .line = getenv("USER"), .history = NULL, .callback = NULL,
196 strncpy(username, readline(& prompt), 255);
198 set(& rc, "username", username);
201 if(!(password = getpass("Password: ")))
202 exit(EXIT_FAILURE);
204 set(& rc, "password", password);
208 memset(& data, 0, sizeof(struct hash));
209 memset(& track, 0, sizeof(struct hash));
210 memset(& playlist, 0, sizeof(struct playlist));
212 /* Fork to background. */
213 if(background) {
214 int null;
215 pid_t pid = fork();
217 if(pid == -1) {
218 fputs("Failed to daemonize.\n", stderr);
219 exit(EXIT_FAILURE);
220 } else if(pid) {
221 exit(EXIT_SUCCESS);
224 enable(QUIET);
226 /* Detach from TTY */
227 setsid();
228 pid = fork();
230 if(pid > 0)
231 exit(EXIT_SUCCESS);
233 /* Close stdin out and err */
234 close(0);
235 close(1);
236 close(2);
238 /* Redirect stdin and out to /dev/null */
239 null = open("/dev/null", O_RDWR);
240 dup(null);
241 dup(null);
244 ppid = getpid();
246 atexit(cleanup);
247 loadqueue(!0);
249 /* Set up signal handlers for communication with the playback process. */
250 signal(SIGINT, forcequit);
252 /* SIGUSR2 from playfork means it detected an error. */
253 signal(SIGUSR2, playsig);
255 /* Catch SIGTSTP to set pausetime when user suspends us with ^Z. */
256 signal(SIGTSTP, stopsig);
259 /* Authenticate to the Last.FM server. */
260 if(!authenticate(value(& rc, "username"), value(& rc, "password")))
261 exit(EXIT_FAILURE);
263 /* Store session key for use by external tools. */
264 if(haskey(& data, "session")) {
265 FILE * fd = fopen(rcpath("session"), "w");
266 if(fd) {
267 fprintf(fd, "%s\n", value(& data, "session"));
268 fclose(fd);
273 /* Play default radio, if specified. */
274 if(haskey(& rc, "default-radio"))
275 station(value(& rc, "default-radio"));
278 /* The main loop. */
279 while(!0) {
280 pid_t child;
281 int status, playnext = 0;
284 /* Check if anything died (submissions fork or playback fork). */
285 while((child = waitpid(-1, & status, WNOHANG | WUNTRACED | WCONTINUED)) > 0) {
286 if(child == subfork)
287 subdead(WEXITSTATUS(status));
288 else if(child == playfork) {
289 if(WIFSTOPPED(status)) {
290 /* time(& pausetime); */
292 else {
293 if(WIFCONTINUED(status)) {
294 signal(SIGTSTP, stopsig);
295 if(pausetime != 0) {
296 pauselength += time(NULL) - pausetime;
299 else {
300 playnext = !0;
301 unlinknp();
303 pausetime = 0;
309 Check if the playback process died. If so, submit the data
310 of the last played track. Then check if there are tracks left
311 in the playlist; if not, try to refresh the list and stop the
312 stream if there are no new tracks to fetch.
313 Also handle user stopping the stream here. We need to check for
314 playnext != 0 before handling enabled(STOPPED) anyway, otherwise
315 playfork would still be running.
317 if(playnext) {
318 playfork = 0;
320 if(enabled(RTP)) {
321 unsigned duration, played, minimum;
323 duration = atoi(value(& track, "duration")) / 1000;
324 played = time(NULL) - changeTime - pauselength;
326 /* Allow user to specify minimum playback length (min. 50%). */
327 if(haskey(& rc, "minimum")) {
328 unsigned percent = atoi(value(& rc, "minimum"));
329 if(percent < 50)
330 percent = 50;
331 minimum = duration * percent / 100;
333 else {
334 minimum = duration / 2;
337 if(duration >= 30 && (played >= 240 || played > minimum))
338 enqueue(& track);
341 submit(value(& rc, "username"), value(& rc, "password"));
343 /* Check if the user stopped the stream. */
344 if(enabled(STOPPED) || error) {
345 freelist(& playlist);
346 empty(& track);
348 if(error) {
349 fputs("Playback stopped with an error.\n", stderr);
350 error = 0;
353 disable(STOPPED);
354 disable(CHANGED);
356 continue;
359 shift(& playlist);
363 if(playnext || enabled(CHANGED)) {
364 if(nextstation != NULL) {
365 playnext = 0;
366 disable(CHANGED);
368 station(nextstation);
370 free(nextstation);
371 nextstation = NULL;
374 if(!playlist.left) {
375 expand(& playlist);
376 if(!playlist.left) {
377 puts("No tracks left.");
378 playnext = 0;
379 disable(CHANGED);
380 continue;
384 if(!playfork) {
385 if(play(& playlist)) {
386 time(& changeTime);
387 pauselength = 0;
389 set(& track, "stationURL", currentStation);
391 /* Print what's currently played. (Ondrej Novy) */
392 if(!background) {
393 if(enabled(CHANGED) && playlist.left > 0) {
394 puts(meta("Receiving %s.", !0, & track));
395 disable(CHANGED);
398 if(haskey(& rc, "title-format"))
399 printf("%s\n", meta(value(& rc, "title-format"), !0, & track));
400 else
401 printf("%s\n", meta("Now playing \"%t\" by %a.", !0, & track));
405 /* Write track data into a file. */
406 if(haskey(& rc, "np-file") && haskey(& rc, "np-file-format")) {
407 signed np;
408 const char
409 * file = value(& rc, "np-file"),
410 * fmt = value(& rc, "np-file-format");
412 unlink(file);
413 if(-1 != (np = open(file, O_WRONLY | O_CREAT, 0644))) {
414 const char * output = meta(fmt, 0, & track);
415 if(output)
416 write(np, output, strlen(output));
417 close(np);
422 if(haskey(& rc, "screen-format")) {
423 const char * term = getenv("TERM");
424 if(term != NULL && !strncmp(term, "screen", 6)) {
425 const char * output =
426 meta(value(& rc, "screen-format"), 0, & track);
427 printf("\x1Bk%s\x1B\\", output);
432 if(haskey(& rc, "term-format")) {
433 const char * output =
434 meta(value(& rc, "term-format"), 0, & track);
435 printf("\x1B]2;%s\a", output);
439 /* Run a command with our track data. */
440 if(haskey(& rc, "np-cmd"))
441 run(meta(value(& rc, "np-cmd"), 0, & track));
442 } else
443 changeTime = 0;
446 if(banned(value(& track, "creator"))) {
447 puts(meta("%a is banned.", !0, & track));
448 rate("B");
449 fflush(stdout);
453 playnext = 0;
455 if(playfork && changeTime && haskey(& track, "duration") && !pausetime) {
456 unsigned duration;
457 signed remain;
458 char remstr[32];
460 duration = atoi(value(& track, "duration")) / 1000;
462 remain = (changeTime + duration) - time(NULL) + pauselength;
464 snprintf(remstr, sizeof(remstr), "%d", remain);
465 set(& track, "remain", remstr);
467 if(!background) {
468 printf(
469 "%c%02d:%02d%c",
470 remain < 0 ? '-' : ' ',
471 (remain >= 0) ? (remain / 60) : (-remain / 60),
472 (remain >= 0) ? (remain % 60) : (-remain % 60),
473 batch ? '\n' : '\r'
475 fflush(stdout);
479 interface(!background);
480 if(haveSocket)
481 sckif(background ? 2 : 0, -1);
484 return 0;
488 static void help(const char * argv0, int errorCode) {
489 fprintf(stderr,
490 "Shell.FM v" PACKAGE_VERSION ", (C) 2006-2008 by Jonas Kramer\n"
491 "Published under the terms of the GNU General Public License (GPL).\n"
492 "\n"
493 "%s [options] [lastfm://url]\n"
494 "\n"
495 " -d daemon mode.\n"
496 " -i address to listen on.\n"
497 " -p port to listen on.\n"
498 " -b batch mode.\n"
499 " -D device to play on.\n"
500 " -y proxy url to connect through.\n"
501 " -h this help.\n",
502 argv0
505 exit(errorCode);
509 static void cleanup(void) {
510 canon(!0);
511 rmsckif();
513 if(haskey(& rc, "unix") && getpid() == ppid)
514 unlink(value(& rc, "unix"));
516 unlinknp();
518 empty(& data);
519 empty(& rc);
520 empty(& track);
522 freelist(& playlist);
524 if(currentStation) {
525 free(currentStation);
526 currentStation = NULL;
529 if(subfork)
530 waitpid(subfork, NULL, 0);
532 dumpqueue(!0);
534 /* Clean cache. */
535 if(!access(rcpath("cache"), R_OK | W_OK | X_OK)) {
536 const char * cache = rcpath("cache");
537 DIR * directory = opendir(cache);
539 if(directory != NULL) {
540 time_t expiry = 24 * 60 * 60; /* Expiration after 24h. */
541 struct dirent * entry;
542 struct stat status;
543 char path[PATH_MAX];
545 if(haskey(& rc, "expiry"))
546 expiry = atoi(value(& rc, "expiry"));
548 while((entry = readdir(directory)) != NULL) {
549 snprintf(path, sizeof(path), "%s/%s", cache, entry->d_name);
551 if(!stat(path, & status)) {
552 if(status.st_mtime < (time(NULL) - expiry)) {
553 unlink(path);
558 closedir(directory);
562 if(playfork)
563 kill(playfork, SIGUSR1);
567 static void forcequit(int sig) {
568 if(sig == SIGINT) {
569 fputs("Try to press Q next time you want to quit.\n", stderr);
570 exit(-1);
575 static void playsig(int sig) {
576 if(sig == SIGUSR2)
577 error = !0;
581 static void stopsig(int sig) {
582 if(sig == SIGTSTP) {
583 time(& pausetime);
585 signal(SIGTSTP, SIG_DFL);
586 raise(SIGTSTP);
590 static void unlinknp(void) {
591 /* Remove now-playing file. */
592 if(haskey(& rc, "np-file")) {
593 const char * np = value(& rc, "np-file");
594 if(np != NULL)
595 unlink(np);