2 Copyright (C) 2006-2010 by Jonas Kramer
3 Published under the terms of the GNU General Public License (GPL).
17 #include <sys/types.h>
25 #include "interface.h"
46 # define WCONTINUED 0 /* not available on NetBSD */
49 # define WIFCONTINUED(x) ((x) == 0x13) /* SIGCONT */
53 #if !defined(WCONTINUED) || !defined(WIFCONTINUED)
57 # define WIFCONTINUED(wstat) (0)
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);
69 int main(int argc
, char ** argv
) {
70 int option
, nerror
= 0, background
= 0, quiet
= 0, have_socket
= 0;
71 time_t pauselength
= 0;
75 /* Create directories. */
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"))
85 /* Disable RTP if option "no-rtp" is set to something. */
86 if(haskey(& rc
, "no-rtp"))
89 /* If "daemon" is set in the configuration, enable daemon mode by default. */
90 if(haskey(& rc
, "daemon"))
93 /* If "quiet" is set in the configuration, enable quiet mode by default. */
94 if(haskey(& rc
, "quiet"))
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:")))
105 case 'd': /* Daemonize. */
106 background
= !background
;
109 case 'i': /* IP to bind network interface to. */
110 set(& rc
, "bind", optarg
);
113 case 'p': /* Port to listen on. */
115 set(& rc
, "port", optarg
);
117 fputs("Invalid port.\n", stderr
);
122 case 'b': /* Batch mode */
126 case 'D': /* Path to audio device file. */
127 set(& rc
, "device", optarg
);
130 case 'y': /* Proxy address. */
131 set(& rc
, "proxy", optarg
);
134 case 'Y': /* SOCKS proxy address. */
135 set(& rc
, "socks-proxy", optarg
);
138 case 'q': /* Quiet mode. */
142 case 'h': /* Print help text and exit. */
147 fprintf(stderr
, "Missing argument for option -%c.\n\n", optopt
);
153 fprintf(stderr
, "Unknown option -%c.\n", optopt
);
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
);
167 help(argv
[0], EXIT_FAILURE
);
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
);
177 if(!haskey(& rc
, "device"))
178 set(& rc
, "device", "/dev/audio");
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).");
187 puts("\nPress ? for help.\n");
189 puts("Compiled for the use with Shell.FM Wrapper.\n");
195 /* Open a port so Shell.FM can be controlled over network. */
196 if(haskey(& rc
, "bind")) {
199 if(haskey(& rc
, "port"))
200 port
= atoi(value(& rc
, "port"));
202 if(tcpsock(value(& rc
, "bind"), (unsigned short) port
))
207 /* Open a UNIX socket for local "remote" control. */
208 if(haskey(& rc
, "unix") && unixsock(value(& rc
, "unix")))
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
);
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. */
228 fputs("Failed to daemonize.\n", stderr
);
236 /* Detach from TTY */
243 /* Close stdin out and err */
248 /* Redirect stdin and out to /dev/null */
249 null
= open("/dev/null", O_RDWR
);
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. */
272 struct input keyboard
= { 0, KEYBOARD
};
273 register_handle(keyboard
);
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
) {
288 set(& rc
, "default-radio", last
);
292 station(value(& rc
, "default-radio"));
296 radioprompt("radio url> ");
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) {
306 subdead(WEXITSTATUS(status
));
307 else if(child
== playfork
) {
308 if(WIFSTOPPED(status
)) {
309 /* time(& pausetime); */
312 if(WIFCONTINUED(status
)) {
313 signal(SIGTSTP
, stopsig
);
315 pauselength
+= time(NULL
) - pausetime
;
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.
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"));
354 minimum
= duration
* percent
/ 100;
357 minimum
= duration
/ 2;
360 if(duration
>= 30 && (played
>= 240 || played
> minimum
)) {
361 debug("adding track to scrobble queue\n");
365 debug("track too short (duration %d, played %d, minimum %d) - not scrobbling\n", duration
, played
, minimum
);
371 /* Check if the user stopped the stream. */
372 if(enabled(STOPPED
) || error
) {
373 freelist(& playlist
);
377 fputs("Playback stopped with an error.\n", stderr
);
391 if(playnext
|| enabled(CHANGED
)) {
392 if(nextstation
!= NULL
) {
396 if(!station(nextstation
))
403 if(!enabled(STOPPED
) && !playlist
.left
) {
406 puts("No tracks left.");
415 If there was a track played before, and there is a gap
416 configured, wait that many seconds before playing the next
419 if(playnext
&& !enabled(INTERRUPTED
) && haskey(& rc
, "gap")) {
420 int gap
= atoi(value(& rc
, "gap"));
426 disable(INTERRUPTED
);
428 if(play(& playlist
)) {
432 set(& track
, "stationURL", current_station
);
434 /* Print what's currently played. (Ondrej Novy) */
436 if(enabled(CHANGED
) && playlist
.left
> 0) {
437 puts(meta("Receiving %s.", M_COLORED
, & track
));
441 if(haskey(& rc
, "title-format"))
442 printf("%s\n", meta(value(& rc
, "title-format"), M_COLORED
, & track
));
444 printf("%s\n", meta("Now playing \"%t\" by %a.", M_COLORED
, & track
));
448 notify_now_playing(& track
);
452 /* Write track data into a file. */
453 if(haskey(& rc
, "np-file") && haskey(& rc
, "np-file-format")) {
456 * file
= value(& rc
, "np-file"),
457 * fmt
= value(& rc
, "np-file-format");
460 if(-1 != (np
= open(file
, O_WRONLY
| O_CREAT
, 0644))) {
461 const char * output
= meta(fmt
, 0, & track
);
463 write(np
, output
, strlen(output
));
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
));
495 if(banned(value(& track
, "creator"))) {
496 puts(meta("%a is banned.", M_COLORED
, & track
));
504 if(playfork
&& change_time
&& haskey(& track
, "duration") && !pausetime
) {
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
);
519 meta("%r", M_COLORED
, & track
),
520 // strdup(meta("%v", M_COLORED, & track)),
527 handle_input(1000000);
534 static void help(const char * argv0
, int error_code
) {
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"
539 "%s [options] [lastfm://url]\n"
543 " -i address to listen on.\n"
544 " -p port to listen on.\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"
557 static void cleanup(void) {
560 if(haskey(& rc
, "unix") && getpid() == ppid
)
561 unlink(value(& rc
, "unix"));
567 freelist(& playlist
);
569 if(current_station
) {
570 free(current_station
);
571 current_station
= NULL
;
575 waitpid(subfork
, NULL
, 0);
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
;
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
)) {
608 kill(playfork
, SIGUSR1
);
611 static void cleanup_term(void) {
612 if (getpid() == ppid
) {
618 static void forcequit(int sig
) {
620 fputs("Try to press Q next time you want to quit.\n", stderr
);
626 static void playsig(int sig
) {
634 static void stopsig(int sig
) {
638 signal(SIGTSTP
, SIG_DFL
);