1 /*
2  * Copyright (c) 2010, 2011 Ryan Flannery <ryan.flannery@gmail.com>
3  *
4  * Permission to use, copy, modify, and distribute this software for any
5  * purpose with or without fee is hereby granted, provided that the above
6  * copyright notice and this permission notice appear in all copies.
7  *
8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15  */
16 
17 #include "vitunes.h"
18 #include "config.h"     /* NOTE: must be after vitunes.h */
19 #include "socket.h"
20 
21 /*****************************************************************************
22  * GLOBALS, EXPORTED
23  ****************************************************************************/
24 
25 /* key playlists to keep track of */
26 playlist *viewing_playlist;
27 playlist *playing_playlist;
28 
29 /* visual mode start position */
30 int visual_mode_start = -1;
31 
32 /* signal flags */
33 volatile sig_atomic_t VSIG_QUIT = 0;            /* 1 = quit vitunes */
34 volatile sig_atomic_t VSIG_RESIZE = 0;          /* 1 = resize display */
35 volatile sig_atomic_t VSIG_SIGCHLD = 0;         /* 1 = got sigchld */
36 volatile sig_atomic_t VSIG_PLAYER_MONITOR = 0;  /* 1 = update player stats */
37 
38 /*
39  * enum used for QUIT_CAUSE values. Currently only one is used, but might add
40  * more.  see the end of the main function and the signal_handler for how/why
41  * this is used.
42  */
43 enum { EXIT_NORMAL, BAD_PLAYER };
44 volatile sig_atomic_t QUIT_CAUSE = EXIT_NORMAL;
45 
46 /* used with -DDEBUG */
47 FILE  *debug_log;
48 
49 
50 /*****************************************************************************
51  * GLOBALS, LOCAL
52  ****************************************************************************/
53 
54 /* location patterns of various things (%s is user's home directory) */
55 const char *VITUNES_DIR_FMT  = "%s/.vitunes";
56 const char *CONF_FILE_FMT    = "%s/.vitunes/vitunes.conf";
57 const char *DB_FILE_FMT      = "%s/.vitunes/vitunes.db";
58 const char *PLAYLIST_DIR_FMT = "%s/.vitunes/playlists";
59 
60 /* absolute paths of key stuff */
61 char *vitunes_dir;
62 char *conf_file;
63 char *db_file;
64 char *playlist_dir;
65 char *player_backend;
66 
67 
68 /*****************************************************************************
69  * local functions
70  ****************************************************************************/
71 
72 /* misc. functions */
73 int  handle_switches(int argc, char *argv[]);
74 void usage(const char *);
75 void signal_handler(int);
76 void setup_timer();
77 
78 
79 int
main(int argc,char * argv[])80 main(int argc, char *argv[])
81 {
82    char  *home;
83    int    previous_command;
84    int    input;
85    int    sock = -1;
86    fd_set fds;
87 
88 #ifdef DEBUG
89    if ((debug_log = fopen("vitunes-debug.log", "w")) == NULL)
90       err(1, "failed to open debug log");
91 #endif
92 
93    /*------------------------------------------------------------------------
94     * build paths names needed by vitunes & handle switches
95     *----------------------------------------------------------------------*/
96 
97    /* get home dir */
98    if ((home = getenv("HOME")) == NULL)
99       errx(1, "$HOME not set. Can't find my config files.");
100 
101    /* build paths & other needed strings */
102    asprintf(&vitunes_dir,    VITUNES_DIR_FMT,  home);
103    asprintf(&conf_file,      CONF_FILE_FMT,    home);
104    asprintf(&db_file,        DB_FILE_FMT,      home);
105    asprintf(&playlist_dir,   PLAYLIST_DIR_FMT, home);
106    asprintf(&player_backend, "%s", DEFAULT_PLAYER_BACKEND);
107    if (vitunes_dir == NULL || conf_file == NULL
108    ||  db_file == NULL     || playlist_dir == NULL
109    ||  player_backend == NULL)
110       err(1, "failed to create needed file names");
111 
112    /* handle command-line switches & e-commands */
113    handle_switches(argc, argv);
114 
115    if(sock_send_msg(VITUNES_RUNNING) != -1) {
116       printf("Vitunes appears to be running already. Won't open socket.");
117    } else {
118       if((sock = sock_listen()) == -1)
119          errx(1, "failed to open socket.");
120    }
121 
122 
123    /*
124     * IF we've reached here, then there were no e-commands.
125     * start vitunes normally...
126     */
127 
128 
129    /*------------------------------------------------------------------------
130     * initialize stuff
131     *--------------------------------------------------------------------- */
132 
133    /* setup signal handlers (XXX must be before player_init) */
134    signal(SIGPIPE,  SIG_IGN);          /* broken pipe with child (ignore) */
135    signal(SIGCHLD,  signal_handler);   /* child died */
136    signal(SIGHUP,   signal_handler);   /* quit */
137    signal(SIGINT,   signal_handler);   /* quit */
138    signal(SIGQUIT,  signal_handler);   /* quit */
139    signal(SIGTERM,  signal_handler);   /* quit */
140    signal(SIGWINCH, signal_handler);   /* resize */
141    setup_timer();                      /* periodic timer to update player */
142 
143    /* init small stuff (XXX some must be done before medialib_load) */
144    mi_query_init();        /* global query description */
145    mi_sort_init();         /* global sort description */
146    mi_display_init();      /* global display description */
147    ybuffer_init();         /* global yank/copy buffer */
148    toggleset_init();       /* global toggleset (list of toggle-lists) */
149 
150    /* load media library (database and all playlists) & sort */
151    medialib_load(db_file, playlist_dir);
152    if (mdb.library->nfiles == 0) {
153       printf("The vitunes database is currently empty.\n");
154       printf("See 'vitunes -e help add' for how to add files.\n");
155       return 0;
156    }
157 
158    /* apply default sort to library */
159    qsort(mdb.library->files, mdb.library->nfiles, sizeof(meta_info*), mi_compare);
160 
161    /* setup user interface and default colors */
162    kb_init();
163    ui_init(DEFAULT_LIBRARY_WINDOW_WIDTH);
164    paint_setup_colors();
165 
166    /* basic ui setup to get ui started */
167    setup_viewing_playlist(mdb.library);
168    ui.library->nrows  = mdb.nplaylists;
169    playing_playlist = NULL;
170 
171    /* load config file and run commands in it now */
172    load_config();
173 
174    /* start media player child */
175    player_init(player_backend);
176    player_info.mode = DEFAULT_PLAYER_MODE;
177    atexit(player_destroy);
178 
179    /* initial painting of the display */
180    paint_all();
181 
182    /* -----------------------------------------------------------------------
183     * begin input loop
184     * -------------------------------------------------------------------- */
185 
186    previous_command = -1;
187    while (!VSIG_QUIT) {
188       struct timeval  tv;
189 
190       /* handle any signal flags */
191       process_signals();
192 
193       tv.tv_sec = 1;
194       tv.tv_usec = 0;
195 
196       FD_ZERO(&fds);
197       FD_SET(0, &fds);
198       if(sock > 0)
199          FD_SET(sock, &fds);
200       errno = 0;
201       if(select((sock > 0 ? sock : 0) + 1, &fds, NULL, NULL, &tv) == -1) {
202          if(errno == 0 || errno == EINTR)
203             continue;
204          break;
205       }
206 
207       if(sock > 0) {
208          if(FD_ISSET(sock, &fds))
209             sock_recv_and_exec(sock);
210       }
211 
212       if(FD_ISSET(0, &fds)) {
213          /* handle any available input */
214          if ((input = getch()) && input != ERR) {
215             if (isdigit(input) &&  (input != '0' || gnum_get() > 0))
216                gnum_add(input - '0');
217             else if (input == '\n' && gnum_get() > 0 && previous_command >= 0)
218                kb_execute(previous_command);
219             else
220                kb_execute(input);
221          }
222       }
223    }
224 
225    /* -----------------------------------------------------------------------
226     * cleanup
227     * -------------------------------------------------------------------- */
228 
229    ui_destroy();
230    player_destroy();
231    medialib_destroy();
232 
233    mi_query_clear();
234    ybuffer_free();
235    toggleset_free();
236 
237    /* do we have any odd cause for quitting? */
238    if (QUIT_CAUSE != EXIT_NORMAL) {
239       switch (QUIT_CAUSE) {
240          case BAD_PLAYER:
241             warnx("It appears the media player is misbehaving.  Apologies.");
242             break;
243       }
244    }
245 
246    return 0;
247 }
248 
249 /* print proper usage */
250 void
usage(const char * pname)251 usage(const char *pname)
252 {
253    fprintf(stderr,"\
254 usage: %s [-f config-file] [-d database-file] [-p playlist-dir] [-m player-path] [-e COMMAND ...]\n\
255 See \"%s -e help\" for information about what e-commands are available.\n\
256 ",
257    pname, pname);
258    exit(1);
259 }
260 
261 /* actual signal handler */
262 void
signal_handler(int sig)263 signal_handler(int sig)
264 {
265    switch (sig) {
266       case SIGHUP:
267       case SIGINT:
268       case SIGQUIT:
269       case SIGTERM:
270          VSIG_QUIT = 1;
271          break;
272       case SIGALRM:
273          VSIG_PLAYER_MONITOR = 1;
274          break;
275       case SIGWINCH:
276          VSIG_RESIZE = 1;
277          break;
278       case SIGCHLD:
279          VSIG_SIGCHLD = 1;
280          break;
281    }
282 }
283 
284 /* handle any signal flags */
285 void
process_signals()286 process_signals()
287 {
288    static playlist *prev_queue = NULL;
289    static int       prev_qidx = -1;
290    static bool      prev_is_playing = false;
291    static float     prev_volume = -1;
292 
293    /* handle resize event */
294    if (VSIG_RESIZE) {
295       ui_resize();
296       ui_clear();
297       paint_all();
298       VSIG_RESIZE = 0;
299    }
300 
301    /* monitor player */
302    if (VSIG_PLAYER_MONITOR) {
303       player_monitor();
304 
305       if (prev_is_playing || player.playing())
306          paint_player();
307 
308       /* need to repaint anything else? */
309       if (prev_is_playing != player.playing()) {
310          paint_library();
311          paint_playlist();
312       } else if (prev_queue != player_info.queue) {
313          paint_library();
314          if (prev_queue == viewing_playlist) {
315             paint_playlist();
316          }
317       }
318       if (player_info.queue == viewing_playlist
319       &&  prev_qidx != player_info.qidx) {
320          paint_playlist();
321       }
322       if (prev_volume != player.volume()) {
323          paint_message("volume: %3.0f%%", player.volume());
324          prev_volume = player.volume();
325       }
326 
327       prev_queue = player_info.queue;
328       prev_qidx = player_info.qidx;
329       prev_is_playing = player.playing();
330       VSIG_PLAYER_MONITOR = 0;
331    }
332 
333    /* restart player if needed */
334    if (VSIG_SIGCHLD) {
335       if (player.sigchld != NULL) player.sigchld();
336       VSIG_SIGCHLD = 0;
337    }
338 }
339 
340 /* setup timer signal handler above */
341 void
setup_timer()342 setup_timer()
343 {
344    struct sigaction sig_act;
345    struct itimerval timer;
346 
347    /* create timer signal handler */
348    if (sigemptyset(&sig_act.sa_mask) < 0)
349       err(1, "setup_timer: sigemptyset failed");
350 
351    sig_act.sa_flags = 0;
352    sig_act.sa_handler = signal_handler;
353    if (sigaction(SIGALRM, &sig_act, NULL) < 0)
354       err(1, "setup_timer: sigaction failed");
355 
356    /* setup timer details */
357    timer.it_value.tv_sec = 0;
358    timer.it_value.tv_usec = 500000;    /* 1 half-second */
359    timer.it_interval.tv_sec = 0;
360    timer.it_interval.tv_usec = 500000; /* 1 half-second */
361 
362    /* register timer */
363    if (setitimer(ITIMER_REAL, &timer, NULL) < 0)
364       err(1, "setup_timer: setitimer failed");
365 }
366 
367 /*
368  * load config file and execute all command-mode commands within.
369  * XXX note that this requires mdb, ui, and player to all be loaded/setup
370  */
371 void
load_config()372 load_config()
373 {
374    const char *errmsg = NULL;
375    size_t  length, linenum;
376    FILE   *fin;
377    char   *line;
378    char   *copy;
379    char  **argv;
380    int     argc;
381    bool    found;
382    int     found_idx = 0;
383    int     num_matches;
384    int     i, ret;
385 
386    if ((fin = fopen(conf_file, "r")) == NULL)
387       return;
388 
389    linenum = 0;
390    while (!feof(fin)) {
391 
392       /* get next line */
393       if ((line = fparseln(fin, &length, &linenum, NULL, 0)) == NULL) {
394          if (ferror(fin))
395             err(1, "error reading config file '%s'", conf_file);
396          else
397             break;
398       }
399 
400       /* skip whitespace */
401       copy = line;
402       copy += strspn(copy, " \t\n");
403       if (copy[0] == '\0') {
404          free(line);
405          continue;
406       }
407 
408       /* parse line into argc/argv */
409       if (str2argv(copy, &argc, &argv, &errmsg) != 0) {
410          endwin();
411          errx(1, "%s line %zd: parse error: %s", conf_file, linenum, errmsg);
412       }
413 
414       /* run command */
415       found = false;
416       num_matches = 0;
417       for (i = 0; i < CommandPathSize; i++) {
418          if (match_command_name(argv[0], CommandPath[i].name)) {
419             found = true;
420             found_idx = i;
421             num_matches++;
422          }
423       }
424 
425       if (found && num_matches == 1) {
426          if ((ret = (CommandPath[found_idx].func)(argc, argv)) != 0) {
427             endwin();
428             errx(1, "%s line %zd: error with command '%s' [%i]",
429                conf_file, linenum, argv[0], ret);
430          }
431       } else if (num_matches > 1) {
432          endwin();
433          errx(1, "%s line %zd: ambiguous abbreviation '%s'",
434             conf_file, linenum, argv[0]);
435       } else {
436          endwin();
437          errx(1, "%s line %zd: unknown command'%s'",
438             conf_file, linenum, argv[0]);
439       }
440 
441       argv_free(&argc, &argv);
442       free(line);
443    }
444 
445    fclose(fin);
446 }
447 
448 /*
449  * parse the command line and handle all switches.
450  * this also handles all of the e-commands.
451  */
452 int
handle_switches(int argc,char * argv[])453 handle_switches(int argc, char *argv[])
454 {
455    int ch;
456    int i;
457    int had_c_commands = 0;
458 
459    while ((ch = getopt(argc, argv, "he:f:d:p:m:c:")) != -1) {
460       switch (ch) {
461          case 'c':
462             if(sock_send_msg(optarg) == -1)
463                errx(1, "Failed to send message. Vitunes not running?");
464             had_c_commands = 1;
465             break;
466 
467          case 'd':
468             if ((db_file = strdup(optarg)) == NULL)
469                err(1, "handle_switches: strdup db_file failed");
470             break;
471 
472          case 'e':
473             /* an e-command */
474             argc -= optind - 1;
475             argv += optind - 1;
476 
477             for (i = 0; i < ECMD_PATH_SIZE; i++) {
478                if (strcmp(optarg, ECMD_PATH[i].name) == 0)
479                   exit((ECMD_PATH[i].func)(argc, argv));
480             }
481 
482             errx(1, "Unknown e-command '%s'.  See 'vitunes -e help' for list.",
483                optarg);
484             break;
485 
486          case 'f':
487             if ((conf_file = strdup(optarg)) == NULL)
488                err(1, "handle_switches: strdup conf_file failed");
489             break;
490 
491          case 'm':
492             if ((player_backend = strdup(optarg)) == NULL)
493                err(1, "handle_switches: strdup player_backend failed");
494             break;
495 
496          case 'p':
497             if ((playlist_dir = strdup(optarg)) == NULL)
498                err(1, "handle_switches: strdup playlist_dir failed");
499             break;
500 
501          case 'h':
502          case '?':
503          default:
504             usage(argv[0]);
505             /* NOT REACHED */
506       }
507    }
508 
509    if(had_c_commands)
510       exit(0);
511 
512    return 0;
513 }
514