2 Copyright (C) 2006-2008 by Jonas Kramer
3 Published under the terms of the GNU General Public License (GPL).
17 #include <sys/types.h>
24 #include "interface.h"
40 # define WCONTINUED 0 /* not available on NetBSD */
43 # define WIFCONTINUED(x) ((x) == 0x13) /* SIGCONT */
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);
62 int main(int argc
, char ** argv
) {
63 int option
, nerror
= 0, background
= 0, haveSocket
= 0;
64 time_t pauselength
= 0;
68 /* Create directories. */
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"))
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:")))
86 case 'd': /* Daemonize. */
90 case 'i': /* IP to bind network interface to. */
91 set(& rc
, "bind", optarg
);
94 case 'p': /* Port to listen on. */
96 set(& rc
, "port", optarg
);
98 fputs("Invalid port.\n", stderr
);
103 case 'b': /* Batch mode */
107 case 'D': /* Path to audio device file. */
108 set(& rc
, "device", optarg
);
111 case 'y': /* Proxy address. */
112 set(& rc
, "proxy", optarg
);
115 case 'h': /* Print help text and exit. */
120 fprintf(stderr
, "Missing argument for option -%c.\n\n", optopt
);
126 fprintf(stderr
, "Unknown option -%c.\n", optopt
);
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
);
139 set(& rc
, "default-radio", station
);
145 help(argv
[0], EXIT_FAILURE
);
148 if(!haskey(& rc
, "device"))
149 set(& rc
, "device", "/dev/audio");
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")) {
164 if(haskey(& rc
, "port"))
165 port
= atoi(value(& rc
, "port"));
167 if(tcpsock(value(& rc
, "bind"), (unsigned short) port
))
172 /* Open a UNIX socket for local "remote" control. */
173 if(haskey(& rc
, "unix") && unixsock(value(& rc
, "unix")))
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
);
184 /* Ask for username/password if they weren't specified in the .rc file. */
185 if(!haskey(& rc
, "password")) {
188 if(!haskey(& rc
, "username")) {
189 char username
[256] = { 0 };
191 struct prompt prompt
= {
193 .line
= getenv("USER"), .history
= NULL
, .callback
= NULL
,
196 strncpy(username
, readline(& prompt
), 255);
198 set(& rc
, "username", username
);
201 if(!(password
= getpass("Password: ")))
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. */
218 fputs("Failed to daemonize.\n", stderr
);
226 /* Detach from TTY */
233 /* Close stdin out and err */
238 /* Redirect stdin and out to /dev/null */
239 null
= open("/dev/null", O_RDWR
);
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")))
263 /* Store session key for use by external tools. */
264 if(haskey(& data
, "session")) {
265 FILE * fd
= fopen(rcpath("session"), "w");
267 fprintf(fd
, "%s\n", value(& data
, "session"));
273 /* Play default radio, if specified. */
274 if(haskey(& rc
, "default-radio"))
275 station(value(& rc
, "default-radio"));
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) {
287 subdead(WEXITSTATUS(status
));
288 else if(child
== playfork
) {
289 if(WIFSTOPPED(status
)) {
290 /* time(& pausetime); */
293 if(WIFCONTINUED(status
)) {
294 signal(SIGTSTP
, stopsig
);
296 pauselength
+= time(NULL
) - pausetime
;
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.
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"));
331 minimum
= duration
* percent
/ 100;
334 minimum
= duration
/ 2;
337 if(duration
>= 30 && (played
>= 240 || played
> minimum
))
341 submit(value(& rc
, "username"), value(& rc
, "password"));
343 /* Check if the user stopped the stream. */
344 if(enabled(STOPPED
) || error
) {
345 freelist(& playlist
);
349 fputs("Playback stopped with an error.\n", stderr
);
363 if(playnext
|| enabled(CHANGED
)) {
364 if(nextstation
!= NULL
) {
368 station(nextstation
);
377 puts("No tracks left.");
385 if(play(& playlist
)) {
389 set(& track
, "stationURL", currentStation
);
391 /* Print what's currently played. (Ondrej Novy) */
393 if(enabled(CHANGED
) && playlist
.left
> 0) {
394 puts(meta("Receiving %s.", !0, & track
));
398 if(haskey(& rc
, "title-format"))
399 printf("%s\n", meta(value(& rc
, "title-format"), !0, & track
));
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")) {
409 * file
= value(& rc
, "np-file"),
410 * fmt
= value(& rc
, "np-file-format");
413 if(-1 != (np
= open(file
, O_WRONLY
| O_CREAT
, 0644))) {
414 const char * output
= meta(fmt
, 0, & track
);
416 write(np
, output
, strlen(output
));
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
));
446 if(banned(value(& track
, "creator"))) {
447 puts(meta("%a is banned.", !0, & track
));
455 if(playfork
&& changeTime
&& haskey(& track
, "duration") && !pausetime
) {
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
);
470 remain
< 0 ? '-' : ' ',
471 (remain
>= 0) ? (remain
/ 60) : (-remain
/ 60),
472 (remain
>= 0) ? (remain
% 60) : (-remain
% 60),
479 interface(!background
);
481 sckif(background
? 2 : 0, -1);
488 static void help(const char * argv0
, int errorCode
) {
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"
493 "%s [options] [lastfm://url]\n"
496 " -i address to listen on.\n"
497 " -p port to listen on.\n"
499 " -D device to play on.\n"
500 " -y proxy url to connect through.\n"
509 static void cleanup(void) {
513 if(haskey(& rc
, "unix") && getpid() == ppid
)
514 unlink(value(& rc
, "unix"));
522 freelist(& playlist
);
525 free(currentStation
);
526 currentStation
= NULL
;
530 waitpid(subfork
, NULL
, 0);
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
;
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
)) {
563 kill(playfork
, SIGUSR1
);
567 static void forcequit(int sig
) {
569 fputs("Try to press Q next time you want to quit.\n", stderr
);
575 static void playsig(int sig
) {
581 static void stopsig(int sig
) {
585 signal(SIGTSTP
, SIG_DFL
);
590 static void unlinknp(void) {
591 /* Remove now-playing file. */
592 if(haskey(& rc
, "np-file")) {
593 const char * np
= value(& rc
, "np-file");