1 /* vi:set ts=8 sts=8 sw=8:
2 *
3 * PMS <<Practical Music Search>>
4 * Copyright (C) 2006-2010 Kim Tore Jensen
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 *
19 *
20 * pms.cpp - The PMS main class
21 *
22 */
23
24 #include "pms.h"
25
26 #if defined(__FreeBSD__) || defined(__DragonFly__)
27 #include <sys/wait.h>
28 #endif
29
30 using namespace std;
31
32 Pms * pms;
33
34
35 /*
36 * 1..2..3..
37 */
main(int argc,char * argv[])38 int main(int argc, char *argv[])
39 {
40 int exitcode;
41
42 pms = new Pms(argc, argv);
43 if (!pms)
44 {
45 printf("Not enough memory, aborting.\n");
46 return PMS_EXIT_LOMEM;
47 }
48
49 exitcode = pms->init();
50 if (exitcode == 0)
51 {
52 exitcode = pms->main();
53 }
54 delete pms;
55 return exitcode;
56 }
57
58 /*
59 * Init
60 */
Pms(int c,char ** v)61 Pms::Pms(int c, char **v)
62 {
63 argc = c;
64 argv = v;
65 disp = NULL;
66 }
67
68 /*
69 * Unit
70 */
~Pms()71 Pms::~Pms()
72 {
73 }
74
75 /*
76 * Connection and main loop
77 */
main()78 int Pms::main()
79 {
80 string t_str;
81 pms_pending_keys pending = PEND_NONE;
82 char pass[512] = "";
83 bool statechanged = false;
84 bool songchanged = false;
85 pms_window * win = NULL;
86 time_t timer = 0;
87
88 /* Connection */
89 printf(_("Connecting to host %s, port %ld..."), options->get_string("hostname").c_str(), options->get_long("port"));
90
91 if (conn->connect() != 0)
92 {
93 printf(_("failed.\n"));
94 printf("%s\n", conn->errorstr().c_str());
95
96 return PMS_EXIT_CANTCONNECT;
97 }
98
99 printf(_("connected.\n"));
100
101 /* Password? */
102 if (options->get_string("password").size() > 0)
103 {
104 printf(_("Sending password..."));
105 if (comm->sendpassword(options->get_string("password")))
106 printf(_("password accepted.\n"));
107 else
108 printf(_("wrong password.\n"));
109 }
110
111 comm->get_available_commands();
112 if (!(comm->authlevel() & AUTH_READ))
113 {
114 printf(_("This mpd server requires a password.\n"));
115 while(true)
116 {
117 printf(_("Password: "));
118
119 fgets(pass, 512, stdin) ? 1 : 0; //ternary here is a hack to get rid of a warn_unused_result warning
120 if (pass[strlen(pass)-1] == '\n')
121 pass[strlen(pass)-1] = '\0';
122
123 options->set_string("password", pass);
124
125 comm->sendpassword(pass);
126 comm->get_available_commands();
127 if (!(comm->authlevel() & AUTH_READ))
128 printf(_("Wrong password, try again.\n"));
129 else
130 break;
131 }
132 }
133
134 printf(_("Successfully logged in.\n"));
135
136 /* Update lists */
137 printf(_("Retrieving library and all playlists..."));
138 comm->update(true);
139 printf(_("done.\n"));
140
141 comm->has_new_library();
142 comm->has_new_playlist();
143 printf(_("Sorting library..."));
144 comm->library()->sort(options->get_string("sort"));
145 printf(_("done.\n"));
146
147 /* Center attention to current song */
148 comm->library()->gotocurrent();
149 comm->playlist()->gotocurrent();
150
151 _shutdown = false;
152 if (!disp->init())
153 {
154 printf(_("Can't initialize display!\n"));
155 return PMS_EXIT_NODISPLAY;
156 }
157
158 /* Workaround for buggy ncurses clearing the screen on first getch() */
159 getch();
160
161 /* Set up library and playlist windows */
162 playlist = disp->create_playlist();
163 library = disp->create_playlist();
164 // dirlist = disp->create_directorylist();
165 if (!playlist || !library)
166 {
167 delete disp;
168 printf(_("Can't initialize windows!\n"));
169 return PMS_EXIT_NOWINDOWS;
170 }
171 playlist->settitle(_("Playlist"));
172 library->settitle(_("Library"));
173 playlist->list = comm->playlist();
174 library->list = comm->library();
175
176 resetstatus(-1);
177 drawstatus();
178
179 playlist->set_column_size();
180 library->set_column_size();
181
182 connect_window_list();
183
184 /* Focus startup list */
185 comm->activatelist(comm->playlist());
186 t_str = options->get_string("startuplist");
187 if (t_str == "library")
188 {
189 comm->activatelist(comm->library());
190 }
191 else if (t_str.size() > 0 && t_str != "playlist")
192 {
193 comm->activatelist(comm->findplaylist(t_str));
194 }
195 disp->activate(disp->findwlist(comm->activelist()));
196
197 disp->forcedraw();
198 disp->refresh();
199
200 /*
201 * Main loop
202 */
203 do
204 {
205 /* Has to have valid connection. */
206 if (!conn->connected() || !comm->update(false) == -1)
207 {
208 if (timer == 0)
209 {
210 log(MSG_STATUS, STERR, "Disconnected from mpd: %s", comm->err());
211 }
212 if (difftime(time(NULL), timer) >= options->get_long("reconnectdelay"))
213 {
214 if (timer != 0)
215 log(MSG_STATUS, STOK, _("Attempting reconnect..."));
216
217 if (conn->connect() != 0)
218 {
219 if (timer != 0)
220 log(MSG_STATUS, STERR, conn->errorstr().c_str());
221
222 time(&timer);
223 continue;
224 }
225 else
226 {
227 log(MSG_STATUS, STOK, _("Reconnected successfully."));
228 comm->clearerror();
229 timer = 0;
230 }
231 }
232 }
233
234 /* Get updated info about state and playlists */
235 if (comm->has_new_library())
236 {
237 log(MSG_STATUS, STOK, _("Library updated."));
238 if (disp->actwin())
239 disp->actwin()->wantdraw = true;
240 library->list->sort(options->get_string("sort"));
241 library->set_column_size();
242 connect_window_list();
243 }
244 if (comm->has_new_playlist())
245 {
246 if (disp->actwin())
247 disp->actwin()->wantdraw = true;
248 playlist->set_column_size();
249 }
250
251 /* Progress to next song? */
252 progress_nextsong();
253
254 /* Any pending keystrokes? */
255 if (input->get_keystroke())
256 {
257 pending = input->dispatch();
258 if (pending != PEND_NONE)
259 {
260 handle_command(pending);
261 comm->update(true);
262 }
263 }
264
265 songchanged = comm->song_changed();
266 statechanged = comm->state_changed();
267 if (songchanged)
268 {
269 /* Cursor follows playback if song changed */
270 if (options->get_bool("followplayback"))
271 {
272 win = disp->findwlist(comm->activelist());
273 if (win)
274 {
275 setwin(win);
276 win->gotocurrent();
277 }
278 }
279 }
280
281 if (statechanged)
282 {
283 /* Shell command when song finishes */
284 if (options->get_string("onplaylistfinish").size() > 0 && cursong() && cursong()->pos == comm->playlist()->end())
285 {
286 /* If a manual stop was issued, don't do anything */
287 if (comm->status()->state == MPD_STATUS_STATE_STOP && pending != PEND_STOP)
288 {
289 /* soak up return value to suppress
290 * warning */
291 int code = system(options->get_string("onplaylistfinish").c_str());
292 }
293 }
294 }
295
296
297 /* Reset status */
298 if (resetstatus(0) >= options->get_long("resetstatus") || songchanged || statechanged)
299 drawstatus();
300
301 /* Draw XTerm window title */
302 disp->set_xterm_title();
303
304 /* Check out mediator events */
305 /* FIXME: add these into their appropriate places */
306 if (mediator->changed("setting.sort"))
307 comm->library()->sort(options->get_string("sort"));
308 else if (mediator->changed("setting.ignorecase"))
309 comm->library()->sort(options->get_string("sort"));
310 else if (mediator->changed("setting.columns"))
311 disp->actwin()->set_column_size();
312 else if (mediator->changed("setting.mouse"))
313 disp->setmousemask();
314 else if (mediator->changed("redraw.topbar"))
315 disp->resized();
316 else if (mediator->changed("topbarvisible"))
317 disp->resized();
318 else if (mediator->changed("topbarborders"))
319 disp->resized();
320 else if (mediator->changed("topbarspace"))
321 disp->resized();
322 else if (mediator->changed("columnspace"))
323 disp->resized();
324 else if (mediator->changed("setting.topbarclear"))
325 {
326 if (options->get_bool("topbarclear"))
327 options->topbar.clear();
328 }
329
330 /* Draw */
331 disp->topbar->wantdraw = true;
332 if (mediator->changed("redraw"))
333 disp->forcedraw();
334 else
335 disp->draw();
336 disp->refresh();
337
338 }
339 while (!_shutdown);
340
341 log(MSG_CONSOLE, STOK, _("Shutting down program.\n"));
342
343 delete disp;
344 delete comm;
345 delete conn;
346
347 /* Unclutter the prompt */
348 printf("\n");
349
350 return PMS_EXIT_SUCCESS;
351 }
352
353 /*
354 * Set up neccessary variables
355 */
init()356 int Pms::init()
357 {
358 string str;
359 vector<string> * tok;
360
361 int exitcode = PMS_EXIT_SUCCESS;
362 char * host;
363 char * port;
364 char * password;
365 const char * charset = NULL;
366
367 /* Internal pointers */
368 msg = new Message();
369 mediator = new Mediator();
370 interface = new Interface();
371 formatter = new Formatter();
372
373 /* Setup locales and internationalization */
374 setlocale(LC_ALL, "");
375 setlocale(LC_CTYPE, "");
376 g_get_charset(&charset);
377 bindtextdomain(GETTEXT_PACKAGE, LOCALE_DIR);
378 bind_textdomain_codeset(GETTEXT_PACKAGE, charset);
379 textdomain(GETTEXT_PACKAGE);
380
381 /* Print program header */
382 printf("%s v%s\n%s\n", PMS_NAME, PACKAGE_VERSION, PMS_COPYRIGHT);
383
384 /* Read important environment variables */
385 host = getenv("MPD_HOST");
386 port = getenv("MPD_PORT");
387 password = getenv("MPD_PASSWORD");
388
389 /* Set up field types */
390 fieldtypes = new Fieldtypes();
391 fieldtypes->add("num", _("#"), FIELD_NUM, 0, NULL);
392 fieldtypes->add("file", _("Filename"), FIELD_FILE, 0, sort_compare_file);
393 fieldtypes->add("artist", _("Artist"), FIELD_ARTIST, 0, sort_compare_artist);
394 fieldtypes->add("artistsort", _("Artist sort name"), FIELD_ARTISTSORT, 0, sort_compare_artistsort);
395 fieldtypes->add("albumartist", _("Album artist"), FIELD_ALBUMARTIST, 0, sort_compare_albumartist);
396 fieldtypes->add("albumartistsort", _("Album artist sort name"), FIELD_ALBUMARTISTSORT, 0, sort_compare_albumartistsort);
397 fieldtypes->add("title", _("Title"), FIELD_TITLE, 0, sort_compare_title);
398 fieldtypes->add("album", _("Album"), FIELD_ALBUM, 0, sort_compare_album);
399 fieldtypes->add("track", _("Track"), FIELD_TRACK, 6, sort_compare_track);
400 fieldtypes->add("trackshort", _("No"), FIELD_TRACKSHORT, 3, sort_compare_track);
401 fieldtypes->add("length", _("Length"), FIELD_TIME, 7, sort_compare_length);
402 fieldtypes->add("date", _("Date"), FIELD_DATE, 11, sort_compare_date);
403 fieldtypes->add("year", _("Year"), FIELD_YEAR, 5, sort_compare_year);
404 fieldtypes->add("name", _("Name"), FIELD_NAME, 0, sort_compare_name);
405 fieldtypes->add("genre", _("Genre"), FIELD_GENRE, 0, sort_compare_genre);
406 fieldtypes->add("composer", _("Composer"), FIELD_COMPOSER, 0, sort_compare_composer);
407 fieldtypes->add("performer", _("Performer"), FIELD_PERFORMER, 0, sort_compare_performer);
408 fieldtypes->add("disc", _("Disc"), FIELD_DISC, 5, sort_compare_disc);
409 fieldtypes->add("comment", _("Comment"), FIELD_COMMENT, 0, sort_compare_comment);
410
411 /* Set up default bindings */
412 if (!init_commandmap())
413 {
414 return PMS_EXIT_NOCOMMAND;
415 }
416 options = new Options();
417 init_default_keymap();
418
419 /* Our configuration */
420 config = new Configurator(options, bindings);
421
422 /* Some default options */
423 options->set_string("hostname", (host ? host : "127.0.0.1"));
424 if (!password && host)
425 {
426 tok = splitstr(host, "@");
427 if (tok->size() == 2)
428 {
429 options->set_string("hostname", (*tok)[0]);
430 options->set_string("password", (*tok)[1]);
431 }
432 delete tok;
433 }
434 if (options->get_string("password").size() == 0)
435 {
436 options->set_string("password", (password ? password : ""));
437 }
438 options->set_long("port", (port ? atoi(port) : 6600));
439
440 if (options->get_long("port") <= 0 || options->get_long("port") > 65535)
441 {
442 printf(_("Error: port number in environment variable MPD_PORT must be from 1-65535\n"));
443 return PMS_EXIT_BADARGS;
444 }
445
446 /* Parse command-line */
447 if (parse_args(argc, argv) == false)
448 {
449 return PMS_EXIT_BADARGS;
450 }
451
452 if (!config->loadconfigs())
453 return PMS_EXIT_CONFIGERR;
454
455 /* Seed random number generator */
456 srand(time(NULL));
457
458 /* Setup some important stuff */
459 conn = new Connection(options->get_string("hostname"), options->get_long("port"), options->get_long("mpd_timeout"));
460 comm = new Control(conn);
461 disp = new Display(comm);
462 input = new Input();
463 if (!conn || !comm || !disp || !input)
464 return PMS_EXIT_LOMEM;
465
466 /* Initialization finished */
467 return PMS_EXIT_SUCCESS;
468 }
469
470
471
472
473
474
475 /*
476 * Converts long to string
477 */
tostring(long number)478 string Pms::tostring(long number)
479 {
480 ostringstream s;
481 s << number;
482 return s.str();
483 }
484
485 /*
486 * Converts size_t to string
487 */
tostring(size_t number)488 string Pms::tostring(size_t number)
489 {
490 ostringstream s;
491 s << number;
492 return s.str();
493 }
494
495 /*
496 * Converts int to string
497 */
tostring(int number)498 string Pms::tostring(int number)
499 {
500 ostringstream s;
501 s << number;
502 return s.str();
503 }
504
505 /*
506 * Split a string into tokens
507 */
splitstr(string str,string delimiter)508 vector<string> * Pms::splitstr(string str, string delimiter)
509 {
510 vector<string> * tokens = new vector<string>;
511
512 string::size_type last = str.find_first_not_of(delimiter, 0);
513 string::size_type pos = str.find_first_of(delimiter, last);
514
515 while (string::npos != pos || string::npos != last)
516 {
517 tokens->push_back(str.substr(last, pos - last));
518 last = str.find_first_not_of(delimiter, pos);
519 pos = str.find_first_of(delimiter, last);
520 }
521
522 return tokens;
523 }
524
525 /*
526 * Join tokens into a string
527 */
joinstr(vector<string> * source,vector<string>::iterator start,vector<string>::iterator end,string delimiter)528 string Pms::joinstr(vector<string> * source, vector<string>::iterator start, vector<string>::iterator end, string delimiter)
529 {
530 string dest = "";
531
532 while (start != source->end())
533 {
534 dest += *start;
535
536 if (start == end)
537 break;
538
539 if (++start != end)
540 {
541 dest += delimiter;
542 }
543 }
544
545 return dest;
546 }
547
548 /*
549 * Formats seconds into the format Dd H:MM:SS.
550 */
timeformat(int seconds)551 string Pms::timeformat(int seconds)
552 {
553 static const int day = (60 * 60 * 24);
554 static const int hour = (60 * 60);
555 static const int minute = 60;
556
557 int i;
558 string s = "";
559
560 /* No time */
561 if (seconds < 0)
562 {
563 s = "--:--";
564 return s;
565 }
566
567 /* days */
568 if (seconds >= day)
569 {
570 i = seconds / day;
571 s = Pms::tostring(i) + "d ";
572 seconds %= day;
573 }
574
575 /* hours */
576 if (seconds >= hour)
577 {
578 i = seconds / hour;
579 s += zeropad(i, 1) + ":";
580 seconds %= hour;
581 }
582
583 /* minutes */
584 i = seconds / minute;
585 s = s + zeropad(i, 2) + ":";
586 seconds %= minute;
587
588 /* seconds */
589 s += zeropad(seconds, 2);
590
591 return s;
592 }
593
594 /*
595 * Return "song" or "songs" based on plural or not
596 */
pluralformat(unsigned int i)597 string Pms::pluralformat(unsigned int i)
598 {
599 if (i == 1)
600 return _("song");
601 else
602 return _("songs");
603 }
604 /*
605 * Pad integer with zeroes up to target length
606 */
zeropad(int i,unsigned int target)607 string Pms::zeropad(int i, unsigned int target)
608 {
609 string s;
610 s = Pms::tostring(i);
611 while(s.size() < target)
612 s = '0' + s;
613 return s;
614 }
615
616 /*
617 * Replaces % with %%
618 */
formtext(string text)619 string Pms::formtext(string text)
620 {
621 string::const_iterator i;
622 string nutext;
623
624 i = text.begin();
625 nutext.clear();
626
627 while (i != text.end())
628 {
629 nutext += *i;
630 if (*i == '%')
631 nutext += *i;
632 ++i;
633 }
634
635 return nutext;
636 }
637
638 /*
639 * Return true if the terminal supports Unicode
640 */
unicode()641 bool Pms::unicode()
642 {
643 const char * charset = NULL;
644
645 g_get_charset(&charset);
646 return strcmp(charset, "UTF-8") == 0;
647 }
648
649
650
651
652
653
654
655
656
657
658 /*
659 * Run a shell command
660 *
661 * FIXME: perhaps this command should be within Interface class?
662 * TODO: add %artist% tags through the field pattern parser: meaning %file% -> filename, not % -> filename
663 * ...but current implementation is nice and vim-like
664 */
run_shell(string cmd)665 bool Pms::run_shell(string cmd)
666 {
667 string search;
668 string replace;
669 string::size_type pos;
670 int i;
671 Songlist * list;
672 char c;
673
674 msg->clear();
675
676 /*
677 * %: path to current song, not enclosed in quotes
678 */
679 if (cursong())
680 {
681 search = "%";
682 replace = options->get_string("libraryroot");
683 replace += cursong()->file;
684 pos = 0;
685 while ((pos = cmd.find(search, pos)) != string::npos)
686 {
687 if (pos == 0 || cmd[pos - 1] != '\\')
688 cmd.replace(pos, search.size(), replace);
689 pos++;
690 }
691 }
692
693 /*
694 * ##: path to each song in selection (or each song on the current
695 * playlist if there is no selection), each enclosed with doublequotes
696 * and separated by spaces
697 */
698 list = disp->actwin()->plist();
699 search = "##";
700 if (cmd.find(search, 0) != string::npos && list && list->size())
701 {
702 replace = "";
703 for (i = 0; i < list->size(); i++)
704 {
705 if (!list->selection.size || list->song(i)->selected)
706 {
707 replace += options->get_string("libraryroot");
708 replace += list->song(i)->file;
709 replace += "\" \"";
710 }
711 }
712 if (replace.size() > 0)
713 {
714 replace = "\"" + replace.substr(0, replace.size() - 2);
715 pos = 0;
716 while ((pos = cmd.find(search, pos)) != string::npos)
717 {
718 if (pos == 0 || cmd[pos - 1] != '\\')
719 cmd.replace(pos, search.size(), replace);
720 pos++;
721 }
722 }
723 }
724
725 /*
726 * #: path to song the cursor is on, not enclosed in quotes
727 */
728 if (disp->cursorsong())
729 {
730 search = "#";
731 replace = options->get_string("libraryroot");
732 replace += disp->cursorsong()->file;
733 pos = 0;
734 while ((pos = cmd.find(search, pos)) != string::npos)
735 {
736 if (pos == 0 || cmd[pos - 1] != '\\')
737 cmd.replace(pos, search.size(), replace);
738 pos++;
739 }
740 }
741
742 //pms->log(MSG_DEBUG, 0, "running shell command '%s'\n", cmd.c_str());
743 endwin();
744
745 msg->code = system(cmd.c_str());
746 msg->code = WEXITSTATUS(msg->code);
747
748 pms->log(MSG_DEBUG, 0, "Shell returned %d\n", msg->code);
749 if (msg->code != 0)
750 printf(_("\nShell returned %d\n"), msg->code);
751
752 printf(_("\nPress ENTER to continue"));
753 fflush(stdout);
754 {
755 /* soak up return value to suppress warning */
756 int key = scanf("%c", &c);
757 }
758
759 reset_prog_mode();
760 refresh();
761
762 return true;
763 }
764
765 /*
766 * Returns the currently playing song
767 */
cursong()768 Song * Pms::cursong()
769 {
770 if (!comm) return NULL;
771 return comm->song();
772 }
773
774 /*
775 * Reset status to its original state
776 */
drawstatus()777 void Pms::drawstatus()
778 {
779 if (input->mode() == INPUT_JUMP)
780 log(MSG_STATUS, STOK, "/%s", formtext(input->text).c_str());
781 else if (input->mode() == INPUT_FILTER)
782 log(MSG_STATUS, STOK, ":g/%s", formtext(input->text).c_str());
783 else if (input->mode() == INPUT_COMMAND)
784 log(MSG_STATUS, STOK, ":%s", formtext(input->text).c_str());
785 else
786 log(MSG_STATUS, STOK, "%s", playstring().c_str());
787
788 resetstatus(-1);
789 }
790
791 /*
792 * Measures time from last statusbar text
793 */
resetstatus(int set)794 int Pms::resetstatus(int set)
795 {
796 static time_t stored = time(NULL);
797 static time_t now = time(NULL);
798
799 if (set == 1)
800 time(&stored);
801 else if (set == -1)
802 stored = 0;
803
804 if (stored == 0)
805 return 0;
806
807 if (time(&now) == -1)
808 return -1;
809
810 return (static_cast<int>(difftime(now, stored)));
811 }
812
813 /*
814 * Return a textual description on how song progression works
815 */
playstring()816 string Pms::playstring()
817 {
818 string s;
819 string list = "<unknown>";
820 bool is_last;
821
822 long playmode = options->get_long("playmode");
823 long repeatmode = options->get_long("repeat");
824
825 if (!comm->status() || !conn->connected())
826 {
827 s = "Not connected.";
828 return s;
829 }
830
831 if (comm->status()->state == MPD_STATUS_STATE_STOP || !cursong())
832 {
833 s = "Stopped.";
834 return s;
835 }
836 else if (comm->status()->state == MPD_STATUS_STATE_PAUSE)
837 {
838 s = "Paused...";
839 return s;
840 }
841
842 if (comm->activelist())
843 list = comm->activelist()->filename;
844
845 if (list.size() == 0)
846 {
847 if (comm->activelist() == comm->library())
848 list = "library";
849 else if (comm->activelist() == comm->playlist())
850 list = "playlist";
851 }
852
853 s = "Playing ";
854
855 if (playmode == PLAYMODE_MANUAL)
856 {
857 s += "this song, then stopping.";
858 return s;
859 }
860
861 if (repeatmode == REPEAT_ONE)
862 {
863 s += "the same song indefinitely.";
864 return s;
865 }
866
867 is_last = (cursong()->pos == static_cast<int>(comm->playlist()->end()));
868
869 if (!is_last && !(comm->activelist() == comm->playlist() && repeatmode == REPEAT_LIST))
870 {
871 s += "through playlist, then ";
872 }
873
874 if (playmode == PLAYMODE_RANDOM)
875 {
876 s += "random songs from " + list + ".";
877 return s;
878 }
879
880 if (repeatmode == REPEAT_LIST)
881 {
882 s += "songs from " + list + " repeatedly.";
883 return s;
884 }
885
886 if (repeatmode == REPEAT_NONE)
887 {
888 if (comm->activelist() == comm->playlist())
889 {
890 if (options->get_bool("followcursor"))
891 {
892 if (is_last)
893 s += "this song, then ";
894
895 s += "following cursor.";
896 return s;
897 }
898
899 if (is_last)
900 s += "this song, then stopping.";
901 else
902 s += "stopping.";
903
904 return s;
905 }
906 else
907 {
908 s += "songs from " + list + ".";
909 return s;
910 }
911 }
912
913 return s;
914 }
915
916 /*
917 * Put an arbitrary message into the message log
918 */
putlog(Message * m)919 void Pms::putlog(Message * m)
920 {
921 if (m->code == 0 && m->str.size() == 0)
922 return;
923
924 log(MSG_CONSOLE, m->code, m->str.c_str());
925 }
926
927 /*
928 * Log a message.
929 * Verbosity levels:
930 * 0 = statusbar
931 * 1 = console
932 * 2 = debug
933 */
log(int verbosity,long code,const char * format,...)934 void Pms::log(int verbosity, long code, const char * format, ...)
935 {
936 long loglines;
937 va_list ap;
938 char buffer[1024];
939 char tbuffer[20];
940 string level;
941 Message * m;
942 tm * timeinfo;
943 color * pair;
944
945 if (verbosity >= MSG_DEBUG && !pms->options->get_bool("debug"))
946 return;
947
948 m = new Message();
949 if (m == NULL)
950 return;
951
952 va_start(ap, format);
953 vsprintf(buffer, format, ap);
954 va_end(ap);
955
956 m->str = buffer;
957 m->code = code;
958
959 if (verbosity == MSG_STATUS)
960 {
961 m->str += "\n";
962
963 if (code == STOK)
964 pair = options->colors->status;
965 else
966 pair = options->colors->status_error;
967
968 disp->statusbar->clear(false, pair);
969 colprint(disp->statusbar, 0, 0, pair, "%s", buffer);
970 resetstatus(1);
971 }
972
973 if (verbosity <= MSG_DEBUG && pms->options->get_bool("debug"))
974 {
975 timeinfo = localtime(&(m->timestamp));
976 strftime(tbuffer, 20, "%Y-%m-%d %H:%M:%S", timeinfo);
977 if (verbosity == MSG_STATUS)
978 level = "status";
979 else if (verbosity == MSG_CONSOLE)
980 level = "console";
981 else if (verbosity == MSG_DEBUG)
982 level = "debug";
983 fprintf(stderr, "%s /%s/ %s", tbuffer, level.c_str(), m->str.c_str());
984 }
985
986 if (!disp && verbosity < MSG_DEBUG)
987 {
988 printf("%s", buffer);
989 }
990
991 msglog.push_back(m);
992 loglines = options->get_long("msg_buffer_size");
993 if (loglines > 0 && msglog.size() > loglines)
994 msglog.erase(msglog.begin());
995 }
996
997 /*
998 * Checks if time is right for song progression, and takes necessary action
999 */
progress_nextsong()1000 bool Pms::progress_nextsong()
1001 {
1002 static song_t lastid = MPD_SONG_NO_ID;
1003 static Song * lastcursor = NULL;
1004 Songlist * list = NULL;
1005 unsigned int remaining;
1006
1007 long repeatmode;
1008 long playmode;
1009
1010 if (!cursong()) return false;
1011
1012 if (comm->status()->state != MPD_STATUS_STATE_PLAY)
1013 return false;
1014
1015 remaining = (comm->status()->time_total - comm->status()->time_elapsed - comm->status()->crossfade);
1016
1017 repeatmode = options->get_long("repeat");
1018 playmode = options->get_long("playmode");
1019
1020 /* Too early */
1021 if (remaining > options->get_long("nextinterval") || lastid == cursong()->id)
1022 return false;
1023
1024 /* No auto-progression, even when in the middle of playlist */
1025 if (playmode == PLAYMODE_MANUAL)
1026 {
1027 if (remaining <= options->get_long("stopdelay"))
1028 {
1029 pms->log(MSG_DEBUG, 0, "Manual playmode, stopping playback.\n");
1030 comm->stop();
1031 lastid = cursong()->id;
1032 return true;
1033 }
1034 else
1035 {
1036 return false;
1037 }
1038 }
1039 /* Defeat desync with server */
1040 lastid = cursong()->id;
1041
1042 /* Normal progression: reached end of playlist */
1043 if (comm->status()->song == static_cast<int>(playlist->list->end()))
1044 {
1045 /* List to play from */
1046 list = comm->activelist();
1047 if (!list) return false;
1048
1049 if (list == comm->playlist())
1050 {
1051 /* Let MPD handle repeating of the playlist itself */
1052 if (repeatmode == REPEAT_LIST)
1053 return false;
1054
1055 /* Let MPD handle random songs from playlist */
1056 if (playmode == PLAYMODE_RANDOM)
1057 return false;
1058 }
1059
1060 pms->log(MSG_DEBUG, 0, "Auto-progressing to next song.\n");
1061
1062 /* Playback follows cursor */
1063 if (options->get_bool("followcursor") && lastcursor != disp->cursorsong() && disp->cursorsong()->file != cursong()->file)
1064 {
1065 pms->log(MSG_DEBUG, 0, "Playback follows cursor: last cursor=%p, now cursor=%p.\n", lastcursor, disp->cursorsong());
1066 lastcursor = disp->cursorsong();
1067 lastid = comm->add(comm->playlist(), lastcursor);
1068 }
1069
1070 /* Normal song progression */
1071 lastid = playnext(playmode, false);
1072 }
1073
1074 if (lastcursor == NULL)
1075 {
1076 lastcursor = disp->cursorsong();
1077 }
1078
1079 return (lastid != MPD_SONG_NO_ID);
1080 }
1081
1082 /*
1083 * Create new windows for each custom playlist
1084 */
connect_window_list()1085 bool Pms::connect_window_list()
1086 {
1087 bool ok = true;
1088 pms_window * win;
1089 vector<Songlist *>::iterator i;
1090
1091 i = comm->playlists.begin();
1092 while (i != comm->playlists.end())
1093 {
1094 if (disp->findwlist(*i) == NULL)
1095 {
1096 win = disp->create_playlist();
1097 if (win)
1098 win->setplist(*i);
1099 else
1100 ok = false;
1101 }
1102 ++i;
1103 }
1104
1105 return ok;
1106 }
1107
1108 /*
1109 * Default key bindings
1110 */
init_default_keymap()1111 void Pms::init_default_keymap()
1112 {
1113 bindings->clear();
1114
1115 /* Movement */
1116 bindings->add("up", "move-up");
1117 bindings->add("down", "move-down");
1118 bindings->add("pageup", "move-pgup");
1119 bindings->add("pagedown", "move-pgdn");
1120 bindings->add("^B", "move-pgup");
1121 bindings->add("^F", "move-pgdn");
1122 bindings->add("^U", "move-halfpgup");
1123 bindings->add("^D", "move-halfpgdn");
1124 bindings->add("^Y", "scroll-up");
1125 bindings->add("^E", "scroll-down");
1126 bindings->add("z", "center-cursor");
1127 bindings->add("home", "move-home");
1128 bindings->add("end", "move-end");
1129 bindings->add("g", "goto-current");
1130 bindings->add("R", "goto-random");
1131 bindings->add("j", "move-down");
1132 bindings->add("k", "move-up");
1133 bindings->add("t", "prev-window");
1134 bindings->add("T", "next-window");
1135 bindings->add("(", "prev-of album");
1136 bindings->add(")", "next-of album");
1137 bindings->add("{", "prev-of artist");
1138 bindings->add("}", "next-of artist");
1139 bindings->add("1", "change-window playlist");
1140 bindings->add("2", "change-window library");
1141 bindings->add("w", "change-window windowlist");
1142 // TODO: add this for a later version
1143 //bindings->add("W", "change-window directorylist");
1144 bindings->add("tab", "last-window");
1145
1146 /* Searching */
1147 bindings->add("/", "quick-find");
1148 bindings->add("n", "next-result");
1149 bindings->add("N", "prev-result");
1150
1151 /* Playlist management */
1152 bindings->add("a", "add");
1153 bindings->add("A", "add-to");
1154 bindings->add("b", "add-album");
1155 bindings->add("B", "play-album");
1156 bindings->add("delete", "remove");
1157 bindings->add("c", "cropsel");
1158 bindings->add("C", "crop");
1159 bindings->add("insert", "toggle-select");
1160 bindings->add("F12", "activate-list");
1161 bindings->add("^X", "delete-list");
1162 bindings->add("J", "move 1");
1163 bindings->add("K", "move -1");
1164
1165 /* Controls */
1166 bindings->add("return", "play");
1167 bindings->add("kpenter", "play");
1168 bindings->add("backspace", "stop");
1169 bindings->add("p", "pause");
1170 bindings->add("space", "toggle-play");
1171 bindings->add("l", "next");
1172 bindings->add("h", "prev");
1173 bindings->add("M", "mute");
1174 bindings->add("m", "playmode");
1175 bindings->add("r", "repeat");
1176 bindings->add("+", "volume +5");
1177 bindings->add("-", "volume -5");
1178 bindings->add("left", "seek -5");
1179 bindings->add("right", "seek 5");
1180
1181 /* Maintenance */
1182 bindings->add("f", "toggle followcursor");
1183 bindings->add("F", "toggle followplayback");
1184 bindings->add("^F", "toggle followwindow");
1185 bindings->add(":", "command-mode");
1186 bindings->add("u", "update-library");
1187 bindings->add("v", "version");
1188 bindings->add("q", "quit");
1189 bindings->add("F1", "help");
1190 bindings->add("^L", "redraw");
1191 }
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202 /*
1203 * Print the version string
1204 */
print_version()1205 void Pms::print_version()
1206 {
1207 printf("Uses libmpdclient (c) 2003-2007 by Warren Dukes (warren.dukes@gmail.com)\n");
1208 printf("This program is licensed under the GNU General Public License 3.\n");
1209 }
1210
1211 /*
1212 * Print switch usage
1213 */
print_usage()1214 void Pms::print_usage()
1215 {
1216 printf("Usage:\n");
1217 printf(" -%s\t\t\t%s\n", "v", "print version and exit");
1218 printf(" -%s\t\t%s\n", "? --help", "display command-line options");
1219 printf(" -%s\t\t\t%s\n", "d", "turn on debugging to stderr");
1220 printf(" -%s\t\t%s\n", "c <filename>", "use an alternative config file");
1221 printf(" -%s\t\t%s\n", "h <hostname>", "connect to this MPD server");
1222 printf(" -%s\t\t%s\n", "p <port>", "connect to this port");
1223 printf(" -%s\t\t%s\n", "P <password>", "give this password to MPD server");
1224 }
1225
1226 /*
1227 * Helper function, prints an error
1228 */
require_arg(char c)1229 bool Pms::require_arg(char c)
1230 {
1231 printf("Error: option '%c' requires an argument.\n", c);
1232 print_usage();
1233 return false;
1234 }
1235
1236 /*
1237 * Parse command-line arguments
1238 */
parse_args(int argc,char * argv[])1239 bool Pms::parse_args(int argc, char * argv[])
1240 {
1241 int argn;
1242 string value = "";
1243 string arg = "";
1244 bool switched = false;
1245 string s;
1246 string::iterator i;
1247
1248 if (argc <= 1)
1249 return true;
1250
1251 for (argn = 1; argn < argc; argn++)
1252 {
1253 s = argv[argn];
1254
1255 if (s == "--help")
1256 {
1257 print_usage();
1258 return false;
1259 }
1260
1261 i = s.begin();
1262
1263 while (i != s.end())
1264 {
1265 if (!switched)
1266 if (*i != '-')
1267 return false;
1268
1269 switch (*i)
1270 {
1271 case 'd':
1272 options->set_bool("debug", true);
1273 break;
1274 case 'v':
1275 print_version();
1276 return false;
1277 case '?':
1278 print_usage();
1279 return false;
1280 case 'c':
1281 if (++argn >= argc)
1282 return require_arg(*i);
1283 options->set_string("configfile", argv[argn]);
1284 break;
1285 case 'h':
1286 if (++argn >= argc)
1287 return require_arg(*i);
1288 options->set_string("hostname", argv[argn]);
1289 break;
1290 case 'p':
1291 if (++argn >= argc)
1292 return require_arg(*i);
1293 options->set_long("port", atoi(argv[argn]));
1294 if (options->get_long("port") <= 0 || options->get_long("port") > 65535)
1295 {
1296 printf(_("Error: port number must be from 1-65535\n"));
1297 return false;
1298 }
1299 break;
1300 case 'P':
1301 if (++argn >= argc)
1302 return require_arg(*i);
1303 options->set_string("password", argv[argn]);
1304 break;
1305 case '-':
1306 if (switched)
1307 {
1308 print_usage();
1309 return false;
1310 }
1311 switched = true;
1312 break;
1313 default:
1314 printf(_("Error: unknown option '%c'\n"), *i);
1315 print_usage();
1316 return false;
1317 }
1318 ++i;
1319 }
1320
1321 switched = false;
1322 }
1323
1324 return true;
1325 }
1326
1327