2 Copyright (C) 2006-2009 by Jonas Kramer
3 Published under the terms of the GNU General Public License (GPL).
17 #include <sys/types.h>
24 #include "interface.h"
41 # define WCONTINUED 0 /* not available on NetBSD */
44 # define WIFCONTINUED(x) ((x) == 0x13) /* SIGCONT */
48 #if !defined(WCONTINUED) || !defined(WIFCONTINUED)
52 # define WIFCONTINUED(wstat) (0)
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);
69 int main(int argc
, char ** argv
) {
70 int option
, nerror
= 0, background
= 0, haveSocket
= 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 /* 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:")))
101 case 'd': /* Daemonize. */
102 background
= !background
;
105 case 'i': /* IP to bind network interface to. */
106 set(& rc
, "bind", optarg
);
109 case 'p': /* Port to listen on. */
111 set(& rc
, "port", optarg
);
113 fputs("Invalid port.\n", stderr
);
118 case 'b': /* Batch mode */
122 case 'D': /* Path to audio device file. */
123 set(& rc
, "device", optarg
);
126 case 'y': /* Proxy address. */
127 set(& rc
, "proxy", optarg
);
130 case 'h': /* Print help text and exit. */
135 fprintf(stderr
, "Missing argument for option -%c.\n\n", optopt
);
141 fprintf(stderr
, "Unknown option -%c.\n", optopt
);
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
);
155 help(argv
[0], EXIT_FAILURE
);
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
);
165 if(!haskey(& rc
, "device"))
166 set(& rc
, "device", "/dev/audio");
171 puts("Shell.FM v" PACKAGE_VERSION
", (C) 2006-2009 by Jonas Kramer");
172 puts("Published under the terms of the GNU General Public License (GPL).");
175 puts("\nPress ? for help.\n");
177 puts("Compiled for the use with Shell.FM Wrapper.\n");
183 /* Open a port so Shell.FM can be controlled over network. */
184 if(haskey(& rc
, "bind")) {
187 if(haskey(& rc
, "port"))
188 port
= atoi(value(& rc
, "port"));
190 if(tcpsock(value(& rc
, "bind"), (unsigned short) port
))
195 /* Open a UNIX socket for local "remote" control. */
196 if(haskey(& rc
, "unix") && unixsock(value(& rc
, "unix")))
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
);
207 /* Ask for username/password if they weren't specified in the .rc file. */
208 if(!haskey(& rc
, "password")) {
211 if(!haskey(& rc
, "username")) {
212 char username
[256] = { 0 };
214 struct prompt prompt
= {
216 .line
= getenv("USER"), .history
= NULL
, .callback
= NULL
,
219 strncpy(username
, readline(& prompt
), 255);
221 set(& rc
, "username", username
);
224 if(!(password
= getpass("Password: ")))
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. */
241 fputs("Failed to daemonize.\n", stderr
);
249 /* Detach from TTY */
256 /* Close stdin out and err */
261 /* Redirect stdin and out to /dev/null */
262 null
= open("/dev/null", O_RDWR
);
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")))
286 /* Store session key for use by external tools. */
287 if(haskey(& data
, "session")) {
288 FILE * fd
= fopen(rcpath("session"), "w");
290 fprintf(fd
, "%s\n", value(& data
, "session"));
296 /* Play default radio, if specified. */
297 if(haskey(& rc
, "default-radio"))
298 station(value(& rc
, "default-radio"));
300 radioprompt("radio url> ");
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) {
310 subdead(WEXITSTATUS(status
));
311 else if(child
== playfork
) {
312 if(WIFSTOPPED(status
)) {
313 /* time(& pausetime); */
316 if(WIFCONTINUED(status
)) {
317 signal(SIGTSTP
, stopsig
);
319 pauselength
+= time(NULL
) - pausetime
;
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.
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"));
358 minimum
= duration
* percent
/ 100;
361 minimum
= duration
/ 2;
364 if(duration
>= 30 && (played
>= 240 || played
> minimum
))
368 submit(value(& rc
, "username"), value(& rc
, "password"));
370 /* Check if the user stopped the stream. */
371 if(enabled(STOPPED
) || error
) {
372 freelist(& playlist
);
376 fputs("Playback stopped with an error.\n", stderr
);
390 if(playnext
|| enabled(CHANGED
)) {
391 if(nextstation
!= NULL
) {
395 if(!station(nextstation
))
402 if(!enabled(STOPPED
) && !playlist
.left
) {
405 puts("No tracks left.");
414 If there was a track played before, and there is a gap
415 configured, wait that many seconds before playing the next
418 if(playnext
&& !enabled(INTERRUPTED
) && haskey(& rc
, "gap")) {
419 int gap
= atoi(value(& rc
, "gap"));
425 disable(INTERRUPTED
);
427 if(play(& playlist
)) {
431 set(& track
, "stationURL", currentStation
);
433 /* Print what's currently played. (Ondrej Novy) */
435 if(enabled(CHANGED
) && playlist
.left
> 0) {
436 puts(meta("Receiving %s.", M_COLORED
, & track
));
440 if(haskey(& rc
, "title-format"))
441 printf("%s\n", meta(value(& rc
, "title-format"), M_COLORED
, & track
));
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")) {
451 * file
= value(& rc
, "np-file"),
452 * fmt
= value(& rc
, "np-file-format");
455 if(-1 != (np
= open(file
, O_WRONLY
| O_CREAT
, 0644))) {
456 const char * output
= meta(fmt
, 0, & track
);
458 write(np
, output
, strlen(output
));
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
));
488 if(banned(value(& track
, "creator"))) {
489 puts(meta("%a is banned.", M_COLORED
, & track
));
497 if(playfork
&& changeTime
&& haskey(& track
, "duration") && !pausetime
) {
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
);
512 remain
< 0 ? '-' : ' ',
513 (remain
>= 0) ? (remain
/ 60) : (-remain
/ 60),
514 (remain
>= 0) ? (remain
% 60) : (-remain
% 60),
521 interface(!background
);
523 sckif(background
? 2 : 0, -1);
530 static void help(const char * argv0
, int errorCode
) {
532 "Shell.FM v" PACKAGE_VERSION
", (C) 2006-2009 by Jonas Kramer\n"
533 "Published under the terms of the GNU General Public License (GPL).\n"
535 "%s [options] [lastfm://url]\n"
538 " -i address to listen on.\n"
539 " -p port to listen on.\n"
541 " -D device to play on.\n"
542 " -y proxy url to connect through.\n"
551 static void cleanup(void) {
555 if(haskey(& rc
, "unix") && getpid() == ppid
)
556 unlink(value(& rc
, "unix"));
562 freelist(& playlist
);
565 free(currentStation
);
566 currentStation
= NULL
;
570 waitpid(subfork
, NULL
, 0);
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
;
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
)) {
603 kill(playfork
, SIGUSR1
);
607 static void forcequit(int sig
) {
609 fputs("Try to press Q next time you want to quit.\n", stderr
);
615 static void playsig(int sig
) {
623 static void stopsig(int sig
) {
627 signal(SIGTSTP
, SIG_DFL
);